Compare commits
37 Commits
2025.9.0-r
...
develop
Author | SHA1 | Date |
---|---|---|
|
c5f9c0ce5c | |
|
cce302ae4f | |
|
e0d210e15b | |
|
0b7634b126 | |
|
d1446d195a | |
|
218070eb13 | |
|
0f8c068e84 | |
|
69d66b89f2 | |
|
211365de64 | |
|
966127c63e | |
|
54800971eb | |
|
13d5c6d2b2 | |
|
2cff00eedd | |
|
3fc2261041 | |
|
18d66c0233 | |
|
2f52c20150 | |
|
9d70c9ad78 | |
|
42b2aea533 | |
|
97adf6f2cc | |
|
93ff209c51 | |
|
5fe08d0bbb | |
|
8c413d01e6 | |
|
b231da7c7c | |
|
df3e44f62e | |
|
e504560477 | |
|
bcb2073715 | |
|
6a80c23a50 | |
|
2621f468ff | |
|
d4654dd7bd | |
|
b7da6cad87 | |
|
5b4115e21a | |
|
c174c5c144 | |
|
aebc3f781e | |
|
f60b6291d7 | |
|
7673874675 | |
|
6e3354f95d | |
|
b9df928097 |
|
@ -105,6 +105,16 @@ port: 3000
|
||||||
# socket: /path/to/misskey.sock
|
# socket: /path/to/misskey.sock
|
||||||
# chmodSocket: '777'
|
# 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 └────────────────────────────────
|
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||||
|
|
||||||
|
|
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -1,3 +1,28 @@
|
||||||
|
## 2025.9.1
|
||||||
|
|
||||||
|
### NOTE
|
||||||
|
- pnpm 10.16.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
|
## 2025.9.0
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
|
|
@ -3194,6 +3194,7 @@ _imageEffector:
|
||||||
mirror: "Mirror"
|
mirror: "Mirror"
|
||||||
invert: "Invert Colors"
|
invert: "Invert Colors"
|
||||||
grayscale: "Grayscale"
|
grayscale: "Grayscale"
|
||||||
|
blur: "Blur"
|
||||||
colorAdjust: "Color Correction"
|
colorAdjust: "Color Correction"
|
||||||
colorClamp: "Color Compression"
|
colorClamp: "Color Compression"
|
||||||
colorClampAdvanced: "Color Compression (Advanced)"
|
colorClampAdvanced: "Color Compression (Advanced)"
|
||||||
|
@ -3209,6 +3210,8 @@ _imageEffector:
|
||||||
angle: "Angle"
|
angle: "Angle"
|
||||||
scale: "Size"
|
scale: "Size"
|
||||||
size: "Size"
|
size: "Size"
|
||||||
|
radius: "Radius"
|
||||||
|
samples: "Samples"
|
||||||
color: "Color"
|
color: "Color"
|
||||||
opacity: "Opacity"
|
opacity: "Opacity"
|
||||||
normalize: "Normalize"
|
normalize: "Normalize"
|
||||||
|
|
|
@ -1030,6 +1030,10 @@ export interface Locale extends ILocale {
|
||||||
* 処理中
|
* 処理中
|
||||||
*/
|
*/
|
||||||
"processing": string;
|
"processing": string;
|
||||||
|
/**
|
||||||
|
* 準備中
|
||||||
|
*/
|
||||||
|
"preprocessing": string;
|
||||||
/**
|
/**
|
||||||
* プレビュー
|
* プレビュー
|
||||||
*/
|
*/
|
||||||
|
@ -1227,7 +1231,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"noMoreHistory": string;
|
"noMoreHistory": string;
|
||||||
/**
|
/**
|
||||||
* チャットを始める
|
* メッセージを送る
|
||||||
*/
|
*/
|
||||||
"startChat": string;
|
"startChat": string;
|
||||||
/**
|
/**
|
||||||
|
@ -1927,7 +1931,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"markAsReadAllUnreadNotes": string;
|
"markAsReadAllUnreadNotes": string;
|
||||||
/**
|
/**
|
||||||
* すべてのチャットを既読にする
|
* すべてのダイレクトメッセージを既読にする
|
||||||
*/
|
*/
|
||||||
"markAsReadAllTalkMessages": string;
|
"markAsReadAllTalkMessages": string;
|
||||||
/**
|
/**
|
||||||
|
@ -5282,6 +5286,10 @@ export interface Locale extends ILocale {
|
||||||
* 下書き
|
* 下書き
|
||||||
*/
|
*/
|
||||||
"draft": string;
|
"draft": string;
|
||||||
|
/**
|
||||||
|
* 下書きと予約投稿
|
||||||
|
*/
|
||||||
|
"draftsAndScheduledNotes": string;
|
||||||
/**
|
/**
|
||||||
* リアクションする際に確認する
|
* リアクションする際に確認する
|
||||||
*/
|
*/
|
||||||
|
@ -5390,6 +5398,14 @@ export interface Locale extends ILocale {
|
||||||
* チャット
|
* チャット
|
||||||
*/
|
*/
|
||||||
"chat": string;
|
"chat": string;
|
||||||
|
/**
|
||||||
|
* ダイレクトメッセージ
|
||||||
|
*/
|
||||||
|
"directMessage": string;
|
||||||
|
/**
|
||||||
|
* メッセージ
|
||||||
|
*/
|
||||||
|
"directMessage_short": string;
|
||||||
/**
|
/**
|
||||||
* 旧設定情報を移行
|
* 旧設定情報を移行
|
||||||
*/
|
*/
|
||||||
|
@ -5501,6 +5517,14 @@ export interface Locale extends ILocale {
|
||||||
* 低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。
|
* 低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。
|
||||||
*/
|
*/
|
||||||
"defaultImageCompressionLevel_description": string;
|
"defaultImageCompressionLevel_description": string;
|
||||||
|
/**
|
||||||
|
* デフォルトの圧縮度
|
||||||
|
*/
|
||||||
|
"defaultCompressionLevel": string;
|
||||||
|
/**
|
||||||
|
* 低くすると品質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、品質は低下します。
|
||||||
|
*/
|
||||||
|
"defaultCompressionLevel_description": string;
|
||||||
/**
|
/**
|
||||||
* 分
|
* 分
|
||||||
*/
|
*/
|
||||||
|
@ -5529,6 +5553,60 @@ export interface Locale extends ILocale {
|
||||||
* ベータ版の検証にご協力いただきありがとうございます!
|
* ベータ版の検証にご協力いただきありがとうございます!
|
||||||
*/
|
*/
|
||||||
"thankYouForTestingBeta": string;
|
"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": {
|
"_order": {
|
||||||
/**
|
/**
|
||||||
* 新しい順
|
* 新しい順
|
||||||
|
@ -5540,6 +5618,10 @@ export interface Locale extends ILocale {
|
||||||
"oldest": string;
|
"oldest": string;
|
||||||
};
|
};
|
||||||
"_chat": {
|
"_chat": {
|
||||||
|
/**
|
||||||
|
* メッセージ
|
||||||
|
*/
|
||||||
|
"messages": string;
|
||||||
/**
|
/**
|
||||||
* まだメッセージはありません
|
* まだメッセージはありません
|
||||||
*/
|
*/
|
||||||
|
@ -5549,36 +5631,36 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"newMessage": string;
|
"newMessage": string;
|
||||||
/**
|
/**
|
||||||
* 個人チャット
|
* 個別
|
||||||
*/
|
*/
|
||||||
"individualChat": string;
|
"individualChat": string;
|
||||||
/**
|
/**
|
||||||
* 特定ユーザーとの一対一のチャットができます。
|
* 特定ユーザーと個別にメッセージのやりとりができます。
|
||||||
*/
|
*/
|
||||||
"individualChat_description": string;
|
"individualChat_description": string;
|
||||||
/**
|
/**
|
||||||
* ルームチャット
|
* グループ
|
||||||
*/
|
*/
|
||||||
"roomChat": string;
|
"roomChat": string;
|
||||||
/**
|
/**
|
||||||
* 複数人でのチャットができます。
|
* 複数人でメッセージのやりとりができます。
|
||||||
* また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。
|
* また、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。
|
||||||
*/
|
*/
|
||||||
"roomChat_description": string;
|
"roomChat_description": string;
|
||||||
/**
|
/**
|
||||||
* ルームを作成
|
* グループを作成
|
||||||
*/
|
*/
|
||||||
"createRoom": string;
|
"createRoom": string;
|
||||||
/**
|
/**
|
||||||
* ユーザーを招待してチャットを始めましょう
|
* ユーザーを招待してメッセージを送信しましょう
|
||||||
*/
|
*/
|
||||||
"inviteUserToChat": string;
|
"inviteUserToChat": string;
|
||||||
/**
|
/**
|
||||||
* 作成したルーム
|
* 作成したグループ
|
||||||
*/
|
*/
|
||||||
"yourRooms": string;
|
"yourRooms": string;
|
||||||
/**
|
/**
|
||||||
* 参加中のルーム
|
* 参加中のグループ
|
||||||
*/
|
*/
|
||||||
"joiningRooms": string;
|
"joiningRooms": string;
|
||||||
/**
|
/**
|
||||||
|
@ -5598,7 +5680,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"noHistory": string;
|
"noHistory": string;
|
||||||
/**
|
/**
|
||||||
* ルームはありません
|
* グループはありません
|
||||||
*/
|
*/
|
||||||
"noRooms": string;
|
"noRooms": string;
|
||||||
/**
|
/**
|
||||||
|
@ -5618,7 +5700,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"ignore": string;
|
"ignore": string;
|
||||||
/**
|
/**
|
||||||
* ルームから退出
|
* グループから退出
|
||||||
*/
|
*/
|
||||||
"leave": string;
|
"leave": string;
|
||||||
/**
|
/**
|
||||||
|
@ -5642,35 +5724,35 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"newline": string;
|
"newline": string;
|
||||||
/**
|
/**
|
||||||
* このルームをミュート
|
* このグループをミュート
|
||||||
*/
|
*/
|
||||||
"muteThisRoom": string;
|
"muteThisRoom": string;
|
||||||
/**
|
/**
|
||||||
* ルームを削除
|
* グループを削除
|
||||||
*/
|
*/
|
||||||
"deleteRoom": string;
|
"deleteRoom": string;
|
||||||
/**
|
/**
|
||||||
* このサーバー、またはこのアカウントでチャットは有効化されていません。
|
* このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。
|
||||||
*/
|
*/
|
||||||
"chatNotAvailableForThisAccountOrServer": string;
|
"chatNotAvailableForThisAccountOrServer": string;
|
||||||
/**
|
/**
|
||||||
* このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。
|
* このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。
|
||||||
*/
|
*/
|
||||||
"chatIsReadOnlyForThisAccountOrServer": string;
|
"chatIsReadOnlyForThisAccountOrServer": string;
|
||||||
/**
|
/**
|
||||||
* 相手のアカウントでチャット機能が使えない状態になっています。
|
* 相手のアカウントでダイレクトメッセージが使えない状態になっています。
|
||||||
*/
|
*/
|
||||||
"chatNotAvailableInOtherAccount": string;
|
"chatNotAvailableInOtherAccount": string;
|
||||||
/**
|
/**
|
||||||
* このユーザーとのチャットを開始できません
|
* このユーザーとのダイレクトメッセージを開始できません
|
||||||
*/
|
*/
|
||||||
"cannotChatWithTheUser": string;
|
"cannotChatWithTheUser": string;
|
||||||
/**
|
/**
|
||||||
* チャットが使えない状態になっているか、相手がチャットを開放していません。
|
* ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。
|
||||||
*/
|
*/
|
||||||
"cannotChatWithTheUser_description": string;
|
"cannotChatWithTheUser_description": string;
|
||||||
/**
|
/**
|
||||||
* あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。
|
* あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。
|
||||||
*/
|
*/
|
||||||
"youAreNotAMemberOfThisRoomButInvited": string;
|
"youAreNotAMemberOfThisRoomButInvited": string;
|
||||||
/**
|
/**
|
||||||
|
@ -5678,31 +5760,31 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"doYouAcceptInvitation": string;
|
"doYouAcceptInvitation": string;
|
||||||
/**
|
/**
|
||||||
* チャットする
|
* ダイレクトメッセージ
|
||||||
*/
|
*/
|
||||||
"chatWithThisUser": string;
|
"chatWithThisUser": string;
|
||||||
/**
|
/**
|
||||||
* このユーザーはフォロワーからのみチャットを受け付けています。
|
* このユーザーはフォロワーからのみメッセージを受け付けています。
|
||||||
*/
|
*/
|
||||||
"thisUserAllowsChatOnlyFromFollowers": string;
|
"thisUserAllowsChatOnlyFromFollowers": string;
|
||||||
/**
|
/**
|
||||||
* このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。
|
* このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。
|
||||||
*/
|
*/
|
||||||
"thisUserAllowsChatOnlyFromFollowing": string;
|
"thisUserAllowsChatOnlyFromFollowing": string;
|
||||||
/**
|
/**
|
||||||
* このユーザーは相互フォローのユーザーからのみチャットを受け付けています。
|
* このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。
|
||||||
*/
|
*/
|
||||||
"thisUserAllowsChatOnlyFromMutualFollowing": string;
|
"thisUserAllowsChatOnlyFromMutualFollowing": string;
|
||||||
/**
|
/**
|
||||||
* このユーザーは誰からもチャットを受け付けていません。
|
* このユーザーは誰からもメッセージを受け付けていません。
|
||||||
*/
|
*/
|
||||||
"thisUserNotAllowedChatAnyone": string;
|
"thisUserNotAllowedChatAnyone": string;
|
||||||
/**
|
/**
|
||||||
* チャットを許可する相手
|
* メッセージを許可する相手
|
||||||
*/
|
*/
|
||||||
"chatAllowedUsers": string;
|
"chatAllowedUsers": string;
|
||||||
/**
|
/**
|
||||||
* 自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。
|
* 自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。
|
||||||
*/
|
*/
|
||||||
"chatAllowedUsers_note": string;
|
"chatAllowedUsers_note": string;
|
||||||
"_chatAllowedUsers": {
|
"_chatAllowedUsers": {
|
||||||
|
@ -7856,7 +7938,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"canImportUserLists": string;
|
"canImportUserLists": string;
|
||||||
/**
|
/**
|
||||||
* チャットを許可
|
* ダイレクトメッセージを許可
|
||||||
*/
|
*/
|
||||||
"chatAvailability": string;
|
"chatAvailability": string;
|
||||||
/**
|
/**
|
||||||
|
@ -7875,6 +7957,10 @@ export interface Locale extends ILocale {
|
||||||
* サーバーサイドのノートの下書きの作成可能数
|
* サーバーサイドのノートの下書きの作成可能数
|
||||||
*/
|
*/
|
||||||
"noteDraftLimit": string;
|
"noteDraftLimit": string;
|
||||||
|
/**
|
||||||
|
* 予約投稿の同時作成可能数
|
||||||
|
*/
|
||||||
|
"scheduledNoteLimit": string;
|
||||||
/**
|
/**
|
||||||
* ウォーターマーク機能の使用可否
|
* ウォーターマーク機能の使用可否
|
||||||
*/
|
*/
|
||||||
|
@ -8706,7 +8792,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"badge": string;
|
"badge": string;
|
||||||
/**
|
/**
|
||||||
* チャットの背景
|
* メッセージの背景
|
||||||
*/
|
*/
|
||||||
"messageBg": string;
|
"messageBg": string;
|
||||||
/**
|
/**
|
||||||
|
@ -8733,7 +8819,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"reaction": string;
|
"reaction": string;
|
||||||
/**
|
/**
|
||||||
* チャットのメッセージ
|
* ダイレクトメッセージ
|
||||||
*/
|
*/
|
||||||
"chatMessage": string;
|
"chatMessage": string;
|
||||||
};
|
};
|
||||||
|
@ -9017,11 +9103,11 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"write:following": string;
|
"write:following": string;
|
||||||
/**
|
/**
|
||||||
* チャットを見る
|
* ダイレクトメッセージを見る
|
||||||
*/
|
*/
|
||||||
"read:messaging": string;
|
"read:messaging": string;
|
||||||
/**
|
/**
|
||||||
* チャットを操作する
|
* ダイレクトメッセージを操作する
|
||||||
*/
|
*/
|
||||||
"write:messaging": string;
|
"write:messaging": string;
|
||||||
/**
|
/**
|
||||||
|
@ -9313,11 +9399,11 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"write:report-abuse": string;
|
"write:report-abuse": string;
|
||||||
/**
|
/**
|
||||||
* チャットを操作する
|
* ダイレクトメッセージを操作する
|
||||||
*/
|
*/
|
||||||
"write:chat": string;
|
"write:chat": string;
|
||||||
/**
|
/**
|
||||||
* チャットを閲覧する
|
* ダイレクトメッセージを閲覧する
|
||||||
*/
|
*/
|
||||||
"read:chat": string;
|
"read:chat": string;
|
||||||
};
|
};
|
||||||
|
@ -9543,7 +9629,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"birthdayFollowings": string;
|
"birthdayFollowings": string;
|
||||||
/**
|
/**
|
||||||
* チャット
|
* ダイレクトメッセージ
|
||||||
*/
|
*/
|
||||||
"chat": string;
|
"chat": string;
|
||||||
};
|
};
|
||||||
|
@ -10270,6 +10356,14 @@ export interface Locale extends ILocale {
|
||||||
* アンケートの結果が出ました
|
* アンケートの結果が出ました
|
||||||
*/
|
*/
|
||||||
"pollEnded": string;
|
"pollEnded": string;
|
||||||
|
/**
|
||||||
|
* 予約ノートが投稿されました
|
||||||
|
*/
|
||||||
|
"scheduledNotePosted": string;
|
||||||
|
/**
|
||||||
|
* 予約ノートの投稿に失敗しました
|
||||||
|
*/
|
||||||
|
"scheduledNotePostFailed": string;
|
||||||
/**
|
/**
|
||||||
* 新しい投稿
|
* 新しい投稿
|
||||||
*/
|
*/
|
||||||
|
@ -10283,7 +10377,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"roleAssigned": string;
|
"roleAssigned": string;
|
||||||
/**
|
/**
|
||||||
* チャットルームへ招待されました
|
* ダイレクトメッセージのグループへ招待されました
|
||||||
*/
|
*/
|
||||||
"chatRoomInvitationReceived": string;
|
"chatRoomInvitationReceived": string;
|
||||||
/**
|
/**
|
||||||
|
@ -10396,7 +10490,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"roleAssigned": string;
|
"roleAssigned": string;
|
||||||
/**
|
/**
|
||||||
* チャットルームへ招待された
|
* ダイレクトメッセージのグループへ招待された
|
||||||
*/
|
*/
|
||||||
"chatRoomInvitationReceived": string;
|
"chatRoomInvitationReceived": string;
|
||||||
/**
|
/**
|
||||||
|
@ -10578,7 +10672,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"roleTimeline": string;
|
"roleTimeline": string;
|
||||||
/**
|
/**
|
||||||
* チャット
|
* ダイレクトメッセージ
|
||||||
*/
|
*/
|
||||||
"chat": string;
|
"chat": string;
|
||||||
};
|
};
|
||||||
|
@ -10945,7 +11039,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"deleteGalleryPost": string;
|
"deleteGalleryPost": string;
|
||||||
/**
|
/**
|
||||||
* チャットルームを削除
|
* ダイレクトメッセージのグループを削除
|
||||||
*/
|
*/
|
||||||
"deleteChatRoom": string;
|
"deleteChatRoom": string;
|
||||||
/**
|
/**
|
||||||
|
@ -12219,10 +12313,18 @@ export interface Locale extends ILocale {
|
||||||
* テキスト
|
* テキスト
|
||||||
*/
|
*/
|
||||||
"text": string;
|
"text": string;
|
||||||
|
/**
|
||||||
|
* 二次元コード
|
||||||
|
*/
|
||||||
|
"qr": string;
|
||||||
/**
|
/**
|
||||||
* 位置
|
* 位置
|
||||||
*/
|
*/
|
||||||
"position": string;
|
"position": string;
|
||||||
|
/**
|
||||||
|
* マージン
|
||||||
|
*/
|
||||||
|
"margin": string;
|
||||||
/**
|
/**
|
||||||
* タイプ
|
* タイプ
|
||||||
*/
|
*/
|
||||||
|
@ -12279,6 +12381,10 @@ export interface Locale extends ILocale {
|
||||||
* サブドットの数
|
* サブドットの数
|
||||||
*/
|
*/
|
||||||
"polkadotSubDotDivisions": string;
|
"polkadotSubDotDivisions": string;
|
||||||
|
/**
|
||||||
|
* 空欄にするとアカウントのURLになります
|
||||||
|
*/
|
||||||
|
"leaveBlankToAccountUrl": string;
|
||||||
};
|
};
|
||||||
"_imageEffector": {
|
"_imageEffector": {
|
||||||
/**
|
/**
|
||||||
|
@ -12318,6 +12424,14 @@ export interface Locale extends ILocale {
|
||||||
* 白黒
|
* 白黒
|
||||||
*/
|
*/
|
||||||
"grayscale": string;
|
"grayscale": string;
|
||||||
|
/**
|
||||||
|
* ぼかし
|
||||||
|
*/
|
||||||
|
"blur": string;
|
||||||
|
/**
|
||||||
|
* モザイク
|
||||||
|
*/
|
||||||
|
"pixelate": string;
|
||||||
/**
|
/**
|
||||||
* 色調補正
|
* 色調補正
|
||||||
*/
|
*/
|
||||||
|
@ -12362,6 +12476,10 @@ export interface Locale extends ILocale {
|
||||||
* ティアリング
|
* ティアリング
|
||||||
*/
|
*/
|
||||||
"tearing": string;
|
"tearing": string;
|
||||||
|
/**
|
||||||
|
* 塗りつぶし
|
||||||
|
*/
|
||||||
|
"fill": string;
|
||||||
};
|
};
|
||||||
"_fxProps": {
|
"_fxProps": {
|
||||||
/**
|
/**
|
||||||
|
@ -12376,6 +12494,18 @@ export interface Locale extends ILocale {
|
||||||
* サイズ
|
* サイズ
|
||||||
*/
|
*/
|
||||||
"size": string;
|
"size": string;
|
||||||
|
/**
|
||||||
|
* 半径
|
||||||
|
*/
|
||||||
|
"radius": string;
|
||||||
|
/**
|
||||||
|
* サンプル数
|
||||||
|
*/
|
||||||
|
"samples": string;
|
||||||
|
/**
|
||||||
|
* 位置
|
||||||
|
*/
|
||||||
|
"offset": string;
|
||||||
/**
|
/**
|
||||||
* 色
|
* 色
|
||||||
*/
|
*/
|
||||||
|
@ -12488,6 +12618,10 @@ export interface Locale extends ILocale {
|
||||||
* 黒色にする
|
* 黒色にする
|
||||||
*/
|
*/
|
||||||
"zoomLinesBlack": string;
|
"zoomLinesBlack": string;
|
||||||
|
/**
|
||||||
|
* 円形
|
||||||
|
*/
|
||||||
|
"circle": string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
|
@ -12547,6 +12681,80 @@ export interface Locale extends ILocale {
|
||||||
* 下書き一覧
|
* 下書き一覧
|
||||||
*/
|
*/
|
||||||
"listDrafts": string;
|
"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: {
|
declare const locales: {
|
||||||
|
|
|
@ -253,6 +253,7 @@ noteDeleteConfirm: "このノートを削除しますか?"
|
||||||
pinLimitExceeded: "これ以上ピン留めできません"
|
pinLimitExceeded: "これ以上ピン留めできません"
|
||||||
done: "完了"
|
done: "完了"
|
||||||
processing: "処理中"
|
processing: "処理中"
|
||||||
|
preprocessing: "準備中"
|
||||||
preview: "プレビュー"
|
preview: "プレビュー"
|
||||||
default: "デフォルト"
|
default: "デフォルト"
|
||||||
defaultValueIs: "デフォルト: {value}"
|
defaultValueIs: "デフォルト: {value}"
|
||||||
|
@ -302,7 +303,7 @@ uploadNFiles: "{n}個のファイルをアップロード"
|
||||||
explore: "みつける"
|
explore: "みつける"
|
||||||
messageRead: "既読"
|
messageRead: "既読"
|
||||||
noMoreHistory: "これより過去の履歴はありません"
|
noMoreHistory: "これより過去の履歴はありません"
|
||||||
startChat: "チャットを始める"
|
startChat: "メッセージを送る"
|
||||||
nUsersRead: "{n}人が読みました"
|
nUsersRead: "{n}人が読みました"
|
||||||
agreeTo: "{0}に同意"
|
agreeTo: "{0}に同意"
|
||||||
agree: "同意する"
|
agree: "同意する"
|
||||||
|
@ -477,7 +478,7 @@ notFoundDescription: "指定されたURLに該当するページはありませ
|
||||||
uploadFolder: "既定アップロード先"
|
uploadFolder: "既定アップロード先"
|
||||||
markAsReadAllNotifications: "すべての通知を既読にする"
|
markAsReadAllNotifications: "すべての通知を既読にする"
|
||||||
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
|
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
|
||||||
markAsReadAllTalkMessages: "すべてのチャットを既読にする"
|
markAsReadAllTalkMessages: "すべてのダイレクトメッセージを既読にする"
|
||||||
help: "ヘルプ"
|
help: "ヘルプ"
|
||||||
inputMessageHere: "ここにメッセージを入力"
|
inputMessageHere: "ここにメッセージを入力"
|
||||||
close: "閉じる"
|
close: "閉じる"
|
||||||
|
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします
|
||||||
federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
|
federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
|
||||||
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
|
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
|
||||||
draft: "下書き"
|
draft: "下書き"
|
||||||
|
draftsAndScheduledNotes: "下書きと予約投稿"
|
||||||
confirmOnReact: "リアクションする際に確認する"
|
confirmOnReact: "リアクションする際に確認する"
|
||||||
reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
|
reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
|
||||||
markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?"
|
markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?"
|
||||||
|
@ -1343,6 +1345,8 @@ postForm: "投稿フォーム"
|
||||||
textCount: "文字数"
|
textCount: "文字数"
|
||||||
information: "情報"
|
information: "情報"
|
||||||
chat: "チャット"
|
chat: "チャット"
|
||||||
|
directMessage: "ダイレクトメッセージ"
|
||||||
|
directMessage_short: "メッセージ"
|
||||||
migrateOldSettings: "旧設定情報を移行"
|
migrateOldSettings: "旧設定情報を移行"
|
||||||
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
|
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
|
||||||
compress: "圧縮"
|
compress: "圧縮"
|
||||||
|
@ -1370,6 +1374,8 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示"
|
||||||
hideAllTips: "全ての「ヒントとコツ」を非表示"
|
hideAllTips: "全ての「ヒントとコツ」を非表示"
|
||||||
defaultImageCompressionLevel: "デフォルトの画像圧縮度"
|
defaultImageCompressionLevel: "デフォルトの画像圧縮度"
|
||||||
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
|
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
|
||||||
|
defaultCompressionLevel: "デフォルトの圧縮度"
|
||||||
|
defaultCompressionLevel_description: "低くすると品質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、品質は低下します。"
|
||||||
inMinutes: "分"
|
inMinutes: "分"
|
||||||
inDays: "日"
|
inDays: "日"
|
||||||
safeModeEnabled: "セーフモードが有効です"
|
safeModeEnabled: "セーフモードが有効です"
|
||||||
|
@ -1377,53 +1383,70 @@ pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プ
|
||||||
customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。"
|
customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。"
|
||||||
themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。"
|
themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。"
|
||||||
thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!"
|
thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!"
|
||||||
|
createUserSpecifiedNote: "ユーザー指定ノートを作成"
|
||||||
|
schedulePost: "投稿を予約"
|
||||||
|
scheduleToPostOnX: "{x}に投稿を予約します"
|
||||||
|
scheduledToPostOnX: "{x}に投稿が予約されています"
|
||||||
|
schedule: "予約"
|
||||||
|
scheduled: "予約"
|
||||||
|
|
||||||
|
_compression:
|
||||||
|
_quality:
|
||||||
|
high: "高品質"
|
||||||
|
medium: "中品質"
|
||||||
|
low: "低品質"
|
||||||
|
_size:
|
||||||
|
large: "サイズ大"
|
||||||
|
medium: "サイズ中"
|
||||||
|
small: "サイズ小"
|
||||||
|
|
||||||
_order:
|
_order:
|
||||||
newest: "新しい順"
|
newest: "新しい順"
|
||||||
oldest: "古い順"
|
oldest: "古い順"
|
||||||
|
|
||||||
_chat:
|
_chat:
|
||||||
|
messages: "メッセージ"
|
||||||
noMessagesYet: "まだメッセージはありません"
|
noMessagesYet: "まだメッセージはありません"
|
||||||
newMessage: "新しいメッセージ"
|
newMessage: "新しいメッセージ"
|
||||||
individualChat: "個人チャット"
|
individualChat: "個別"
|
||||||
individualChat_description: "特定ユーザーとの一対一のチャットができます。"
|
individualChat_description: "特定ユーザーと個別にメッセージのやりとりができます。"
|
||||||
roomChat: "ルームチャット"
|
roomChat: "グループ"
|
||||||
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
|
roomChat_description: "複数人でメッセージのやりとりができます。\nまた、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。"
|
||||||
createRoom: "ルームを作成"
|
createRoom: "グループを作成"
|
||||||
inviteUserToChat: "ユーザーを招待してチャットを始めましょう"
|
inviteUserToChat: "ユーザーを招待してメッセージを送信しましょう"
|
||||||
yourRooms: "作成したルーム"
|
yourRooms: "作成したグループ"
|
||||||
joiningRooms: "参加中のルーム"
|
joiningRooms: "参加中のグループ"
|
||||||
invitations: "招待"
|
invitations: "招待"
|
||||||
noInvitations: "招待はありません"
|
noInvitations: "招待はありません"
|
||||||
history: "履歴"
|
history: "履歴"
|
||||||
noHistory: "履歴はありません"
|
noHistory: "履歴はありません"
|
||||||
noRooms: "ルームはありません"
|
noRooms: "グループはありません"
|
||||||
inviteUser: "ユーザーを招待"
|
inviteUser: "ユーザーを招待"
|
||||||
sentInvitations: "送信した招待"
|
sentInvitations: "送信した招待"
|
||||||
join: "参加"
|
join: "参加"
|
||||||
ignore: "無視"
|
ignore: "無視"
|
||||||
leave: "ルームから退出"
|
leave: "グループから退出"
|
||||||
members: "メンバー"
|
members: "メンバー"
|
||||||
searchMessages: "メッセージを検索"
|
searchMessages: "メッセージを検索"
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
send: "送信"
|
send: "送信"
|
||||||
newline: "改行"
|
newline: "改行"
|
||||||
muteThisRoom: "このルームをミュート"
|
muteThisRoom: "このグループをミュート"
|
||||||
deleteRoom: "ルームを削除"
|
deleteRoom: "グループを削除"
|
||||||
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。"
|
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。"
|
||||||
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。"
|
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。"
|
||||||
chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。"
|
chatNotAvailableInOtherAccount: "相手のアカウントでダイレクトメッセージが使えない状態になっています。"
|
||||||
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
|
cannotChatWithTheUser: "このユーザーとのダイレクトメッセージを開始できません"
|
||||||
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
|
cannotChatWithTheUser_description: "ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。"
|
||||||
youAreNotAMemberOfThisRoomButInvited: "あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
|
youAreNotAMemberOfThisRoomButInvited: "あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
|
||||||
doYouAcceptInvitation: "招待を承認しますか?"
|
doYouAcceptInvitation: "招待を承認しますか?"
|
||||||
chatWithThisUser: "チャットする"
|
chatWithThisUser: "ダイレクトメッセージ"
|
||||||
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
|
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみメッセージを受け付けています。"
|
||||||
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。"
|
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。"
|
||||||
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。"
|
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。"
|
||||||
thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。"
|
thisUserNotAllowedChatAnyone: "このユーザーは誰からもメッセージを受け付けていません。"
|
||||||
chatAllowedUsers: "チャットを許可する相手"
|
chatAllowedUsers: "メッセージを許可する相手"
|
||||||
chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。"
|
chatAllowedUsers_note: "自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。"
|
||||||
_chatAllowedUsers:
|
_chatAllowedUsers:
|
||||||
everyone: "誰でも"
|
everyone: "誰でも"
|
||||||
followers: "自分のフォロワーのみ"
|
followers: "自分のフォロワーのみ"
|
||||||
|
@ -2034,11 +2057,12 @@ _role:
|
||||||
canImportFollowing: "フォローのインポートを許可"
|
canImportFollowing: "フォローのインポートを許可"
|
||||||
canImportMuting: "ミュートのインポートを許可"
|
canImportMuting: "ミュートのインポートを許可"
|
||||||
canImportUserLists: "リストのインポートを許可"
|
canImportUserLists: "リストのインポートを許可"
|
||||||
chatAvailability: "チャットを許可"
|
chatAvailability: "ダイレクトメッセージを許可"
|
||||||
uploadableFileTypes: "アップロード可能なファイル種別"
|
uploadableFileTypes: "アップロード可能なファイル種別"
|
||||||
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
|
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
|
||||||
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
|
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
|
||||||
noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数"
|
noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数"
|
||||||
|
scheduledNoteLimit: "予約投稿の同時作成可能数"
|
||||||
watermarkAvailable: "ウォーターマーク機能の使用可否"
|
watermarkAvailable: "ウォーターマーク機能の使用可否"
|
||||||
_condition:
|
_condition:
|
||||||
roleAssignedTo: "マニュアルロールにアサイン済み"
|
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||||
|
@ -2281,7 +2305,7 @@ _theme:
|
||||||
buttonHoverBg: "ボタンの背景 (ホバー)"
|
buttonHoverBg: "ボタンの背景 (ホバー)"
|
||||||
inputBorder: "入力ボックスの縁取り"
|
inputBorder: "入力ボックスの縁取り"
|
||||||
badge: "バッジ"
|
badge: "バッジ"
|
||||||
messageBg: "チャットの背景"
|
messageBg: "メッセージの背景"
|
||||||
fgHighlighted: "強調された文字"
|
fgHighlighted: "強調された文字"
|
||||||
|
|
||||||
_sfx:
|
_sfx:
|
||||||
|
@ -2289,7 +2313,7 @@ _sfx:
|
||||||
noteMy: "ノート(自分)"
|
noteMy: "ノート(自分)"
|
||||||
notification: "通知"
|
notification: "通知"
|
||||||
reaction: "リアクション選択時"
|
reaction: "リアクション選択時"
|
||||||
chatMessage: "チャットのメッセージ"
|
chatMessage: "ダイレクトメッセージ"
|
||||||
|
|
||||||
_soundSettings:
|
_soundSettings:
|
||||||
driveFile: "ドライブの音声を使用"
|
driveFile: "ドライブの音声を使用"
|
||||||
|
@ -2369,8 +2393,8 @@ _permissions:
|
||||||
"write:favorites": "お気に入りを操作する"
|
"write:favorites": "お気に入りを操作する"
|
||||||
"read:following": "フォローの情報を見る"
|
"read:following": "フォローの情報を見る"
|
||||||
"write:following": "フォロー・フォロー解除する"
|
"write:following": "フォロー・フォロー解除する"
|
||||||
"read:messaging": "チャットを見る"
|
"read:messaging": "ダイレクトメッセージを見る"
|
||||||
"write:messaging": "チャットを操作する"
|
"write:messaging": "ダイレクトメッセージを操作する"
|
||||||
"read:mutes": "ミュートを見る"
|
"read:mutes": "ミュートを見る"
|
||||||
"write:mutes": "ミュートを操作する"
|
"write:mutes": "ミュートを操作する"
|
||||||
"write:notes": "ノートを作成・削除する"
|
"write:notes": "ノートを作成・削除する"
|
||||||
|
@ -2443,8 +2467,8 @@ _permissions:
|
||||||
"read:clip-favorite": "クリップのいいねを見る"
|
"read:clip-favorite": "クリップのいいねを見る"
|
||||||
"read:federation": "連合に関する情報を取得する"
|
"read:federation": "連合に関する情報を取得する"
|
||||||
"write:report-abuse": "違反を報告する"
|
"write:report-abuse": "違反を報告する"
|
||||||
"write:chat": "チャットを操作する"
|
"write:chat": "ダイレクトメッセージを操作する"
|
||||||
"read:chat": "チャットを閲覧する"
|
"read:chat": "ダイレクトメッセージを閲覧する"
|
||||||
|
|
||||||
_auth:
|
_auth:
|
||||||
shareAccessTitle: "アプリへのアクセス許可"
|
shareAccessTitle: "アプリへのアクセス許可"
|
||||||
|
@ -2507,7 +2531,7 @@ _widgets:
|
||||||
chooseList: "リストを選択"
|
chooseList: "リストを選択"
|
||||||
clicker: "クリッカー"
|
clicker: "クリッカー"
|
||||||
birthdayFollowings: "今日誕生日のユーザー"
|
birthdayFollowings: "今日誕生日のユーザー"
|
||||||
chat: "チャット"
|
chat: "ダイレクトメッセージ"
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
|
@ -2711,10 +2735,12 @@ _notification:
|
||||||
youReceivedFollowRequest: "フォローリクエストが来ました"
|
youReceivedFollowRequest: "フォローリクエストが来ました"
|
||||||
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
||||||
pollEnded: "アンケートの結果が出ました"
|
pollEnded: "アンケートの結果が出ました"
|
||||||
|
scheduledNotePosted: "予約ノートが投稿されました"
|
||||||
|
scheduledNotePostFailed: "予約ノートの投稿に失敗しました"
|
||||||
newNote: "新しい投稿"
|
newNote: "新しい投稿"
|
||||||
unreadAntennaNote: "アンテナ {name}"
|
unreadAntennaNote: "アンテナ {name}"
|
||||||
roleAssigned: "ロールが付与されました"
|
roleAssigned: "ロールが付与されました"
|
||||||
chatRoomInvitationReceived: "チャットルームへ招待されました"
|
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待されました"
|
||||||
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
||||||
achievementEarned: "実績を獲得"
|
achievementEarned: "実績を獲得"
|
||||||
testNotification: "通知テスト"
|
testNotification: "通知テスト"
|
||||||
|
@ -2744,7 +2770,7 @@ _notification:
|
||||||
receiveFollowRequest: "フォロー申請を受け取った"
|
receiveFollowRequest: "フォロー申請を受け取った"
|
||||||
followRequestAccepted: "フォローが受理された"
|
followRequestAccepted: "フォローが受理された"
|
||||||
roleAssigned: "ロールが付与された"
|
roleAssigned: "ロールが付与された"
|
||||||
chatRoomInvitationReceived: "チャットルームへ招待された"
|
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待された"
|
||||||
achievementEarned: "実績の獲得"
|
achievementEarned: "実績の獲得"
|
||||||
exportCompleted: "エクスポートが完了した"
|
exportCompleted: "エクスポートが完了した"
|
||||||
login: "ログイン"
|
login: "ログイン"
|
||||||
|
@ -2794,7 +2820,7 @@ _deck:
|
||||||
mentions: "メンション"
|
mentions: "メンション"
|
||||||
direct: "指名"
|
direct: "指名"
|
||||||
roleTimeline: "ロールタイムライン"
|
roleTimeline: "ロールタイムライン"
|
||||||
chat: "チャット"
|
chat: "ダイレクトメッセージ"
|
||||||
|
|
||||||
_dialog:
|
_dialog:
|
||||||
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
|
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
|
||||||
|
@ -2897,7 +2923,7 @@ _moderationLogTypes:
|
||||||
deletePage: "ページを削除"
|
deletePage: "ページを削除"
|
||||||
deleteFlash: "Playを削除"
|
deleteFlash: "Playを削除"
|
||||||
deleteGalleryPost: "ギャラリーの投稿を削除"
|
deleteGalleryPost: "ギャラリーの投稿を削除"
|
||||||
deleteChatRoom: "チャットルームを削除"
|
deleteChatRoom: "ダイレクトメッセージのグループを削除"
|
||||||
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
|
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
|
||||||
|
|
||||||
_fileViewer:
|
_fileViewer:
|
||||||
|
@ -3271,7 +3297,9 @@ _watermarkEditor:
|
||||||
opacity: "不透明度"
|
opacity: "不透明度"
|
||||||
scale: "サイズ"
|
scale: "サイズ"
|
||||||
text: "テキスト"
|
text: "テキスト"
|
||||||
|
qr: "二次元コード"
|
||||||
position: "位置"
|
position: "位置"
|
||||||
|
margin: "マージン"
|
||||||
type: "タイプ"
|
type: "タイプ"
|
||||||
image: "画像"
|
image: "画像"
|
||||||
advanced: "高度"
|
advanced: "高度"
|
||||||
|
@ -3286,6 +3314,7 @@ _watermarkEditor:
|
||||||
polkadotSubDotOpacity: "サブドットの不透明度"
|
polkadotSubDotOpacity: "サブドットの不透明度"
|
||||||
polkadotSubDotRadius: "サブドットの大きさ"
|
polkadotSubDotRadius: "サブドットの大きさ"
|
||||||
polkadotSubDotDivisions: "サブドットの数"
|
polkadotSubDotDivisions: "サブドットの数"
|
||||||
|
leaveBlankToAccountUrl: "空欄にするとアカウントのURLになります"
|
||||||
|
|
||||||
_imageEffector:
|
_imageEffector:
|
||||||
title: "エフェクト"
|
title: "エフェクト"
|
||||||
|
@ -3299,6 +3328,8 @@ _imageEffector:
|
||||||
mirror: "ミラー"
|
mirror: "ミラー"
|
||||||
invert: "色の反転"
|
invert: "色の反転"
|
||||||
grayscale: "白黒"
|
grayscale: "白黒"
|
||||||
|
blur: "ぼかし"
|
||||||
|
pixelate: "モザイク"
|
||||||
colorAdjust: "色調補正"
|
colorAdjust: "色調補正"
|
||||||
colorClamp: "色の圧縮"
|
colorClamp: "色の圧縮"
|
||||||
colorClampAdvanced: "色の圧縮(高度)"
|
colorClampAdvanced: "色の圧縮(高度)"
|
||||||
|
@ -3310,11 +3341,15 @@ _imageEffector:
|
||||||
checker: "チェッカー"
|
checker: "チェッカー"
|
||||||
blockNoise: "ブロックノイズ"
|
blockNoise: "ブロックノイズ"
|
||||||
tearing: "ティアリング"
|
tearing: "ティアリング"
|
||||||
|
fill: "塗りつぶし"
|
||||||
|
|
||||||
_fxProps:
|
_fxProps:
|
||||||
angle: "角度"
|
angle: "角度"
|
||||||
scale: "サイズ"
|
scale: "サイズ"
|
||||||
size: "サイズ"
|
size: "サイズ"
|
||||||
|
radius: "半径"
|
||||||
|
samples: "サンプル数"
|
||||||
|
offset: "位置"
|
||||||
color: "色"
|
color: "色"
|
||||||
opacity: "不透明度"
|
opacity: "不透明度"
|
||||||
normalize: "正規化"
|
normalize: "正規化"
|
||||||
|
@ -3343,6 +3378,7 @@ _imageEffector:
|
||||||
zoomLinesThreshold: "集中線の幅"
|
zoomLinesThreshold: "集中線の幅"
|
||||||
zoomLinesMaskSize: "中心径"
|
zoomLinesMaskSize: "中心径"
|
||||||
zoomLinesBlack: "黒色にする"
|
zoomLinesBlack: "黒色にする"
|
||||||
|
circle: "円形"
|
||||||
|
|
||||||
drafts: "下書き"
|
drafts: "下書き"
|
||||||
_drafts:
|
_drafts:
|
||||||
|
@ -3359,3 +3395,23 @@ _drafts:
|
||||||
restoreFromDraft: "下書きから復元"
|
restoreFromDraft: "下書きから復元"
|
||||||
restore: "復元"
|
restore: "復元"
|
||||||
listDrafts: "下書き一覧"
|
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"
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2025.9.0-rc.0",
|
"version": "2025.9.1-alpha.2",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/misskey-dev/misskey.git"
|
"url": "https://github.com/misskey-dev/misskey.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.15.1",
|
"packageManager": "pnpm@10.16.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/frontend-shared",
|
"packages/frontend-shared",
|
||||||
"packages/frontend",
|
"packages/frontend",
|
||||||
|
@ -76,7 +76,7 @@
|
||||||
"eslint": "9.35.0",
|
"eslint": "9.35.0",
|
||||||
"globals": "16.3.0",
|
"globals": "16.3.0",
|
||||||
"ncp": "2.0.0",
|
"ncp": "2.0.0",
|
||||||
"pnpm": "10.15.1",
|
"pnpm": "10.16.0",
|
||||||
"start-server-and-test": "2.1.0"
|
"start-server-and-test": "2.1.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import * as fs from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname, resolve } from 'node:path';
|
import { dirname, resolve } from 'node:path';
|
||||||
import * as yaml from 'js-yaml';
|
import * as yaml from 'js-yaml';
|
||||||
|
import { type FastifyServerOptions } from 'fastify';
|
||||||
import type * as Sentry from '@sentry/node';
|
import type * as Sentry from '@sentry/node';
|
||||||
import type * as SentryVue from '@sentry/vue';
|
import type * as SentryVue from '@sentry/vue';
|
||||||
import type { RedisOptions } from 'ioredis';
|
import type { RedisOptions } from 'ioredis';
|
||||||
|
@ -27,6 +28,7 @@ type Source = {
|
||||||
url?: string;
|
url?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
socket?: string;
|
socket?: string;
|
||||||
|
trustProxy?: FastifyServerOptions['trustProxy'];
|
||||||
chmodSocket?: string;
|
chmodSocket?: string;
|
||||||
disableHsts?: boolean;
|
disableHsts?: boolean;
|
||||||
db: {
|
db: {
|
||||||
|
@ -118,6 +120,7 @@ export type Config = {
|
||||||
url: string;
|
url: string;
|
||||||
port: number;
|
port: number;
|
||||||
socket: string | undefined;
|
socket: string | undefined;
|
||||||
|
trustProxy: FastifyServerOptions['trustProxy'];
|
||||||
chmodSocket: string | undefined;
|
chmodSocket: string | undefined;
|
||||||
disableHsts: boolean | undefined;
|
disableHsts: boolean | undefined;
|
||||||
db: {
|
db: {
|
||||||
|
@ -266,6 +269,7 @@ export function loadConfig(): Config {
|
||||||
url: url.origin,
|
url: url.origin,
|
||||||
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
|
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
|
||||||
socket: config.socket,
|
socket: config.socket,
|
||||||
|
trustProxy: config.trustProxy,
|
||||||
chmodSocket: config.chmodSocket,
|
chmodSocket: config.chmodSocket,
|
||||||
disableHsts: config.disableHsts,
|
disableHsts: config.disableHsts,
|
||||||
host,
|
host,
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
|
||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { BlockingsRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiApp } from '@/models/App.js';
|
import type { MiApp } from '@/models/App.js';
|
||||||
import { concat } from '@/misc/prelude/array.js';
|
import { concat } from '@/misc/prelude/array.js';
|
||||||
|
@ -56,6 +56,7 @@ import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
|
@ -192,6 +193,12 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
@Inject(DI.channelFollowingsRepository)
|
@Inject(DI.channelFollowingsRepository)
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.blockingsRepository)
|
||||||
|
private blockingsRepository: BlockingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.driveFilesRepository)
|
||||||
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
@ -221,6 +228,167 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
|
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
|
@bindThis
|
||||||
public async create(user: {
|
public async create(user: {
|
||||||
id: MiUser['id'];
|
id: MiUser['id'];
|
||||||
|
|
|
@ -5,32 +5,18 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import type { noteVisibilities, noteReactionAcceptances } from '@/types.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MiNoteDraft, NoteDraftsRepository, MiNote, MiDriveFile, MiChannel, UsersRepository, DriveFilesRepository, NotesRepository, BlockingsRepository, ChannelsRepository } from '@/models/_.js';
|
import type { MiNoteDraft, NoteDraftsRepository, MiNote, MiDriveFile, MiChannel, UsersRepository, DriveFilesRepository, NotesRepository, BlockingsRepository, ChannelsRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||||
import { IPoll } from '@/models/Poll.js';
|
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { isRenote, isQuote } from '@/misc/is-renote.js';
|
import { isRenote, isQuote } from '@/misc/is-renote.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
|
||||||
export type NoteDraftOptions = {
|
export type NoteDraftOptions = Omit<MiNoteDraft, 'id' | 'userId' | 'user' | 'reply' | 'renote' | 'channel'>;
|
||||||
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()
|
@Injectable()
|
||||||
export class NoteDraftService {
|
export class NoteDraftService {
|
||||||
|
@ -56,6 +42,7 @@ export class NoteDraftService {
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
|
private queueService: QueueService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,36 +59,43 @@ export class NoteDraftService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async create(me: MiLocalUser, data: NoteDraftOptions): Promise<MiNoteDraft> {
|
public async create(me: MiLocalUser, data: NoteDraftOptions): Promise<MiNoteDraft> {
|
||||||
//#region check draft limit
|
//#region check draft limit
|
||||||
|
const policies = await this.roleService.getUserPolicies(me.id);
|
||||||
|
|
||||||
const currentCount = await this.noteDraftsRepository.countBy({
|
const currentCount = await this.noteDraftsRepository.countBy({
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteDraftLimit) {
|
if (currentCount >= policies.noteDraftLimit) {
|
||||||
throw new IdentifiableError('9ee33bbe-fde3-4c71-9b51-e50492c6b9c8', 'Too many drafts');
|
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
|
//#endregion
|
||||||
|
|
||||||
if (data.poll) {
|
await this.validate(me, data);
|
||||||
if (typeof data.poll.expiresAt === 'number') {
|
|
||||||
if (data.poll.expiresAt < Date.now()) {
|
const draft = await this.noteDraftsRepository.insertOne({
|
||||||
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
|
...data,
|
||||||
}
|
id: this.idService.gen(),
|
||||||
} else if (typeof data.poll.expiredAfter === 'number') {
|
userId: me.id,
|
||||||
data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter);
|
});
|
||||||
}
|
|
||||||
|
if (draft.scheduledAt && draft.isActuallyScheduled) {
|
||||||
|
this.schedule(draft);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: NoteDraftOptions): Promise<MiNoteDraft> {
|
public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: Partial<NoteDraftOptions>): Promise<MiNoteDraft> {
|
||||||
const draft = await this.noteDraftsRepository.findOneBy({
|
const draft = await this.noteDraftsRepository.findOneBy({
|
||||||
id: draftId,
|
id: draftId,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
@ -111,19 +105,36 @@ export class NoteDraftService {
|
||||||
throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft');
|
throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.poll) {
|
//#region check draft limit
|
||||||
if (typeof data.poll.expiresAt === 'number') {
|
const policies = await this.roleService.getUserPolicies(me.id);
|
||||||
if (data.poll.expiresAt < Date.now()) {
|
|
||||||
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
|
if (!draft.isActuallyScheduled && data.isActuallyScheduled) {
|
||||||
}
|
const currentScheduledCount = await this.noteDraftsRepository.countBy({
|
||||||
} else if (typeof data.poll.expiredAfter === 'number') {
|
userId: me.id,
|
||||||
data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter);
|
isActuallyScheduled: true,
|
||||||
|
});
|
||||||
|
if (currentScheduledCount >= policies.scheduledNoteLimit) {
|
||||||
|
throw new IdentifiableError('bacdf856-5c51-4159-b88a-804fa5103be5', 'Too many scheduled notes');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
const appliedDraft = await this.checkAndSetDraftNoteOptions(me, draft, data);
|
await this.validate(me, data);
|
||||||
|
|
||||||
return await this.noteDraftsRepository.save(appliedDraft);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -138,6 +149,8 @@ export class NoteDraftService {
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.noteDraftsRepository.delete(draft.id);
|
await this.noteDraftsRepository.delete(draft.id);
|
||||||
|
|
||||||
|
this.clearSchedule(draftId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -154,27 +167,20 @@ export class NoteDraftService {
|
||||||
return draft;
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 関連エンティティを取得し紐づける部分を共通化する
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async checkAndSetDraftNoteOptions(
|
public async validate(
|
||||||
me: MiLocalUser,
|
me: MiLocalUser,
|
||||||
draft: MiNoteDraft,
|
data: Partial<NoteDraftOptions>,
|
||||||
data: NoteDraftOptions,
|
): Promise<void> {
|
||||||
): Promise<MiNoteDraft> {
|
if (data.pollExpiresAt != null) {
|
||||||
data.visibility ??= 'public';
|
if (data.pollExpiresAt.getTime() < Date.now()) {
|
||||||
data.localOnly ??= false;
|
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
|
||||||
if (data.reactionAcceptance === undefined) data.reactionAcceptance = null;
|
}
|
||||||
if (data.channelId != null) {
|
|
||||||
data.visibility = 'public';
|
|
||||||
data.visibleUserIds = [];
|
|
||||||
data.localOnly = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let appliedDraft = draft;
|
|
||||||
|
|
||||||
//#region visibleUsers
|
//#region visibleUsers
|
||||||
let visibleUsers: MiUser[] = [];
|
let visibleUsers: MiUser[] = [];
|
||||||
if (data.visibleUserIds != null) {
|
if (data.visibleUserIds != null && data.visibleUserIds.length > 0) {
|
||||||
visibleUsers = await this.usersRepository.findBy({
|
visibleUsers = await this.usersRepository.findBy({
|
||||||
id: In(data.visibleUserIds),
|
id: In(data.visibleUserIds),
|
||||||
});
|
});
|
||||||
|
@ -184,7 +190,7 @@ export class NoteDraftService {
|
||||||
//#region files
|
//#region files
|
||||||
let files: MiDriveFile[] = [];
|
let files: MiDriveFile[] = [];
|
||||||
const fileIds = data.fileIds ?? null;
|
const fileIds = data.fileIds ?? null;
|
||||||
if (fileIds != null) {
|
if (fileIds != null && fileIds.length > 0) {
|
||||||
files = await this.driveFilesRepository.createQueryBuilder('file')
|
files = await this.driveFilesRepository.createQueryBuilder('file')
|
||||||
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
|
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
@ -288,27 +294,37 @@ export class NoteDraftService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
}
|
||||||
|
|
||||||
appliedDraft = {
|
@bindThis
|
||||||
...appliedDraft,
|
public async schedule(draft: MiNoteDraft): Promise<void> {
|
||||||
visibility: data.visibility,
|
if (!draft.isActuallyScheduled) return;
|
||||||
cw: data.cw ?? null,
|
if (draft.scheduledAt == null) return;
|
||||||
fileIds: fileIds ?? [],
|
if (draft.scheduledAt.getTime() <= Date.now()) return;
|
||||||
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;
|
|
||||||
|
|
||||||
return appliedDraft;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,13 @@ import {
|
||||||
RelationshipJobData,
|
RelationshipJobData,
|
||||||
UserWebhookDeliverJobData,
|
UserWebhookDeliverJobData,
|
||||||
SystemWebhookDeliverJobData,
|
SystemWebhookDeliverJobData,
|
||||||
|
PostScheduledNoteJobData,
|
||||||
} from '../queue/types.js';
|
} from '../queue/types.js';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
||||||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||||
|
export type PostScheduledNoteQueue = Bull.Queue<PostScheduledNoteJobData>;
|
||||||
export type DeliverQueue = Bull.Queue<DeliverJobData>;
|
export type DeliverQueue = Bull.Queue<DeliverJobData>;
|
||||||
export type InboxQueue = Bull.Queue<InboxJobData>;
|
export type InboxQueue = Bull.Queue<InboxJobData>;
|
||||||
export type DbQueue = Bull.Queue;
|
export type DbQueue = Bull.Queue;
|
||||||
|
@ -41,6 +43,12 @@ const $endedPollNotification: Provider = {
|
||||||
inject: [DI.config],
|
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 = {
|
const $deliver: Provider = {
|
||||||
provide: 'queue:deliver',
|
provide: 'queue:deliver',
|
||||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
|
||||||
|
@ -89,6 +97,7 @@ const $systemWebhookDeliver: Provider = {
|
||||||
providers: [
|
providers: [
|
||||||
$system,
|
$system,
|
||||||
$endedPollNotification,
|
$endedPollNotification,
|
||||||
|
$postScheduledNote,
|
||||||
$deliver,
|
$deliver,
|
||||||
$inbox,
|
$inbox,
|
||||||
$db,
|
$db,
|
||||||
|
@ -100,6 +109,7 @@ const $systemWebhookDeliver: Provider = {
|
||||||
exports: [
|
exports: [
|
||||||
$system,
|
$system,
|
||||||
$endedPollNotification,
|
$endedPollNotification,
|
||||||
|
$postScheduledNote,
|
||||||
$deliver,
|
$deliver,
|
||||||
$inbox,
|
$inbox,
|
||||||
$db,
|
$db,
|
||||||
|
@ -113,6 +123,7 @@ export class QueueModule implements OnApplicationShutdown {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||||
|
@Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
|
||||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||||
@Inject('queue:db') public dbQueue: DbQueue,
|
@Inject('queue:db') public dbQueue: DbQueue,
|
||||||
|
@ -129,6 +140,7 @@ export class QueueModule implements OnApplicationShutdown {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.systemQueue.close(),
|
this.systemQueue.close(),
|
||||||
this.endedPollNotificationQueue.close(),
|
this.endedPollNotificationQueue.close(),
|
||||||
|
this.postScheduledNoteQueue.close(),
|
||||||
this.deliverQueue.close(),
|
this.deliverQueue.close(),
|
||||||
this.inboxQueue.close(),
|
this.inboxQueue.close(),
|
||||||
this.dbQueue.close(),
|
this.dbQueue.close(),
|
||||||
|
|
|
@ -31,6 +31,7 @@ import type {
|
||||||
DbQueue,
|
DbQueue,
|
||||||
DeliverQueue,
|
DeliverQueue,
|
||||||
EndedPollNotificationQueue,
|
EndedPollNotificationQueue,
|
||||||
|
PostScheduledNoteQueue,
|
||||||
InboxQueue,
|
InboxQueue,
|
||||||
ObjectStorageQueue,
|
ObjectStorageQueue,
|
||||||
RelationshipQueue,
|
RelationshipQueue,
|
||||||
|
@ -44,6 +45,7 @@ import type * as Bull from 'bullmq';
|
||||||
export const QUEUE_TYPES = [
|
export const QUEUE_TYPES = [
|
||||||
'system',
|
'system',
|
||||||
'endedPollNotification',
|
'endedPollNotification',
|
||||||
|
'postScheduledNote',
|
||||||
'deliver',
|
'deliver',
|
||||||
'inbox',
|
'inbox',
|
||||||
'db',
|
'db',
|
||||||
|
@ -92,6 +94,7 @@ export class QueueService {
|
||||||
|
|
||||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||||
|
@Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
|
||||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||||
@Inject('queue:db') public dbQueue: DbQueue,
|
@Inject('queue:db') public dbQueue: DbQueue,
|
||||||
|
@ -717,6 +720,7 @@ export class QueueService {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'system': return this.systemQueue;
|
case 'system': return this.systemQueue;
|
||||||
case 'endedPollNotification': return this.endedPollNotificationQueue;
|
case 'endedPollNotification': return this.endedPollNotificationQueue;
|
||||||
|
case 'postScheduledNote': return this.postScheduledNoteQueue;
|
||||||
case 'deliver': return this.deliverQueue;
|
case 'deliver': return this.deliverQueue;
|
||||||
case 'inbox': return this.inboxQueue;
|
case 'inbox': return this.inboxQueue;
|
||||||
case 'db': return this.dbQueue;
|
case 'db': return this.dbQueue;
|
||||||
|
|
|
@ -69,6 +69,7 @@ export type RolePolicies = {
|
||||||
chatAvailability: 'available' | 'readonly' | 'unavailable';
|
chatAvailability: 'available' | 'readonly' | 'unavailable';
|
||||||
uploadableFileTypes: string[];
|
uploadableFileTypes: string[];
|
||||||
noteDraftLimit: number;
|
noteDraftLimit: number;
|
||||||
|
scheduledNoteLimit: number;
|
||||||
watermarkAvailable: boolean;
|
watermarkAvailable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -101,20 +102,22 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
userEachUserListsLimit: 50,
|
userEachUserListsLimit: 50,
|
||||||
rateLimitFactor: 1,
|
rateLimitFactor: 1,
|
||||||
avatarDecorationLimit: 1,
|
avatarDecorationLimit: 1,
|
||||||
canImportAntennas: true,
|
canImportAntennas: false,
|
||||||
canImportBlocking: true,
|
canImportBlocking: false,
|
||||||
canImportFollowing: true,
|
canImportFollowing: false,
|
||||||
canImportMuting: true,
|
canImportMuting: false,
|
||||||
canImportUserLists: true,
|
canImportUserLists: false,
|
||||||
chatAvailability: 'available',
|
chatAvailability: 'available',
|
||||||
uploadableFileTypes: [
|
uploadableFileTypes: [
|
||||||
'text/plain',
|
'text/plain',
|
||||||
|
'text/csv',
|
||||||
'application/json',
|
'application/json',
|
||||||
'image/*',
|
'image/*',
|
||||||
'video/*',
|
'video/*',
|
||||||
'audio/*',
|
'audio/*',
|
||||||
],
|
],
|
||||||
noteDraftLimit: 10,
|
noteDraftLimit: 10,
|
||||||
|
scheduledNoteLimit: 1,
|
||||||
watermarkAvailable: true,
|
watermarkAvailable: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -439,6 +442,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
return [...set];
|
return [...set];
|
||||||
}),
|
}),
|
||||||
noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)),
|
noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)),
|
||||||
|
scheduledNoteLimit: calc('scheduledNoteLimit', vs => Math.max(...vs)),
|
||||||
watermarkAvailable: calc('watermarkAvailable', vs => vs.some(v => v === true)),
|
watermarkAvailable: calc('watermarkAvailable', vs => vs.some(v => v === true)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,6 +117,7 @@ export class MetaEntityService {
|
||||||
ratio: ad.ratio,
|
ratio: ad.ratio,
|
||||||
imageUrl: ad.imageUrl,
|
imageUrl: ad.imageUrl,
|
||||||
dayOfWeek: ad.dayOfWeek,
|
dayOfWeek: ad.dayOfWeek,
|
||||||
|
isSensitive: ad.isSensitive ? true : undefined,
|
||||||
})),
|
})),
|
||||||
notesPerOneAd: instance.notesPerOneAd,
|
notesPerOneAd: instance.notesPerOneAd,
|
||||||
enableEmail: instance.enableEmail,
|
enableEmail: instance.enableEmail,
|
||||||
|
|
|
@ -105,6 +105,8 @@ export class NoteDraftEntityService implements OnModuleInit {
|
||||||
const packed: Packed<'NoteDraft'> = await awaitAll({
|
const packed: Packed<'NoteDraft'> = await awaitAll({
|
||||||
id: noteDraft.id,
|
id: noteDraft.id,
|
||||||
createdAt: this.idService.parse(noteDraft.id).date.toISOString(),
|
createdAt: this.idService.parse(noteDraft.id).date.toISOString(),
|
||||||
|
scheduledAt: noteDraft.scheduledAt?.getTime() ?? null,
|
||||||
|
isActuallyScheduled: noteDraft.isActuallyScheduled,
|
||||||
userId: noteDraft.userId,
|
userId: noteDraft.userId,
|
||||||
user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me),
|
user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me),
|
||||||
text: text,
|
text: text,
|
||||||
|
@ -112,13 +114,13 @@ export class NoteDraftEntityService implements OnModuleInit {
|
||||||
visibility: noteDraft.visibility,
|
visibility: noteDraft.visibility,
|
||||||
localOnly: noteDraft.localOnly,
|
localOnly: noteDraft.localOnly,
|
||||||
reactionAcceptance: noteDraft.reactionAcceptance,
|
reactionAcceptance: noteDraft.reactionAcceptance,
|
||||||
visibleUserIds: noteDraft.visibility === 'specified' ? noteDraft.visibleUserIds : undefined,
|
visibleUserIds: noteDraft.visibleUserIds,
|
||||||
hashtag: noteDraft.hashtag ?? undefined,
|
hashtag: noteDraft.hashtag,
|
||||||
fileIds: noteDraft.fileIds,
|
fileIds: noteDraft.fileIds,
|
||||||
files: packedFiles != null ? this.packAttachedFiles(noteDraft.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(noteDraft.fileIds),
|
files: packedFiles != null ? this.packAttachedFiles(noteDraft.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(noteDraft.fileIds),
|
||||||
replyId: noteDraft.replyId,
|
replyId: noteDraft.replyId,
|
||||||
renoteId: noteDraft.renoteId,
|
renoteId: noteDraft.renoteId,
|
||||||
channelId: noteDraft.channelId ?? undefined,
|
channelId: noteDraft.channelId,
|
||||||
channel: channel ? {
|
channel: channel ? {
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
|
@ -127,6 +129,12 @@ export class NoteDraftEntityService implements OnModuleInit {
|
||||||
allowRenoteToExternal: channel.allowRenoteToExternal,
|
allowRenoteToExternal: channel.allowRenoteToExternal,
|
||||||
userId: channel.userId,
|
userId: channel.userId,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
|
poll: noteDraft.hasPoll ? {
|
||||||
|
choices: noteDraft.pollChoices,
|
||||||
|
multiple: noteDraft.pollMultiple,
|
||||||
|
expiresAt: noteDraft.pollExpiresAt?.toISOString(),
|
||||||
|
expiredAfter: noteDraft.pollExpiredAfter,
|
||||||
|
} : null,
|
||||||
|
|
||||||
...(opts.detail ? {
|
...(opts.detail ? {
|
||||||
reply: noteDraft.replyId ? nullIfEntityNotFound(this.noteEntityService.pack(noteDraft.replyId, me, {
|
reply: noteDraft.replyId ? nullIfEntityNotFound(this.noteEntityService.pack(noteDraft.replyId, me, {
|
||||||
|
@ -138,13 +146,6 @@ export class NoteDraftEntityService implements OnModuleInit {
|
||||||
detail: true,
|
detail: true,
|
||||||
skipHide: opts.skipHide,
|
skipHide: opts.skipHide,
|
||||||
})) : undefined,
|
})) : undefined,
|
||||||
|
|
||||||
poll: noteDraft.hasPoll ? {
|
|
||||||
choices: noteDraft.pollChoices,
|
|
||||||
multiple: noteDraft.pollMultiple,
|
|
||||||
expiresAt: noteDraft.pollExpiresAt?.toISOString(),
|
|
||||||
expiredAfter: noteDraft.pollExpiredAfter,
|
|
||||||
} : undefined,
|
|
||||||
} : {} ),
|
} : {} ),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,18 @@ import type { OnModuleInit } from '@nestjs/common';
|
||||||
import type { UserEntityService } from './UserEntityService.js';
|
import type { UserEntityService } from './UserEntityService.js';
|
||||||
import type { NoteEntityService } from './NoteEntityService.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'] as (typeof groupedNotificationTypes[number])[]);
|
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set([
|
||||||
|
'note',
|
||||||
|
'mention',
|
||||||
|
'reply',
|
||||||
|
'renote',
|
||||||
|
'renote:grouped',
|
||||||
|
'quote',
|
||||||
|
'reaction',
|
||||||
|
'reaction:grouped',
|
||||||
|
'pollEnded',
|
||||||
|
'scheduledNotePosted',
|
||||||
|
] as (typeof groupedNotificationTypes[number])[]);
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationEntityService implements OnModuleInit {
|
export class NotificationEntityService implements OnModuleInit {
|
||||||
|
|
|
@ -54,10 +54,17 @@ export class MiAd {
|
||||||
length: 8192, nullable: false,
|
length: 8192, nullable: false,
|
||||||
})
|
})
|
||||||
public memo: string;
|
public memo: string;
|
||||||
|
|
||||||
@Column('integer', {
|
@Column('integer', {
|
||||||
default: 0, nullable: false,
|
default: 0, nullable: false,
|
||||||
})
|
})
|
||||||
public dayOfWeek: number;
|
public dayOfWeek: number;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public isSensitive: boolean;
|
||||||
|
|
||||||
constructor(data: Partial<MiAd>) {
|
constructor(data: Partial<MiAd>) {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
||||||
|
|
|
@ -126,7 +126,7 @@ export class MiNoteDraft {
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public channel: MiChannel | null;
|
public channel: MiChannel | null;
|
||||||
|
|
||||||
// 以下、Pollについて追加
|
//#region 以下、Pollについて追加
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -151,13 +151,15 @@ export class MiNoteDraft {
|
||||||
})
|
})
|
||||||
public pollExpiredAfter: number | null;
|
public pollExpiredAfter: number | null;
|
||||||
|
|
||||||
// ここまで追加
|
//#endregion
|
||||||
|
|
||||||
constructor(data: Partial<MiNoteDraft>) {
|
@Column('timestamp with time zone', {
|
||||||
if (data == null) return;
|
nullable: true,
|
||||||
|
})
|
||||||
|
public scheduledAt: Date | null;
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(data)) {
|
@Column('boolean', {
|
||||||
(this as any)[k] = v;
|
default: false,
|
||||||
}
|
})
|
||||||
}
|
public isActuallyScheduled: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { MiNote } from './Note.js';
|
||||||
import { MiAccessToken } from './AccessToken.js';
|
import { MiAccessToken } from './AccessToken.js';
|
||||||
import { MiRole } from './Role.js';
|
import { MiRole } from './Role.js';
|
||||||
import { MiDriveFile } from './DriveFile.js';
|
import { MiDriveFile } from './DriveFile.js';
|
||||||
|
import { MiNoteDraft } from './NoteDraft.js';
|
||||||
|
|
||||||
// misskey-js の notificationTypes と同期すべし
|
// misskey-js の notificationTypes と同期すべし
|
||||||
export type MiNotification = {
|
export type MiNotification = {
|
||||||
|
@ -60,6 +61,16 @@ export type MiNotification = {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
notifierId: MiUser['id'];
|
notifierId: MiUser['id'];
|
||||||
noteId: MiNote['id'];
|
noteId: MiNote['id'];
|
||||||
|
} | {
|
||||||
|
type: 'scheduledNotePosted';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
noteId: MiNote['id'];
|
||||||
|
} | {
|
||||||
|
type: 'scheduledNotePostFailed';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
noteDraftId: MiNoteDraft['id'];
|
||||||
} | {
|
} | {
|
||||||
type: 'receiveFollowRequest';
|
type: 'receiveFollowRequest';
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -60,5 +60,10 @@ export const packedAdSchema = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
isSensitive: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -195,6 +195,10 @@ export const packedMetaLiteSchema = {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isSensitive: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const packedNoteDraftSchema = {
|
||||||
},
|
},
|
||||||
cw: {
|
cw: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
userId: {
|
userId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -37,27 +37,23 @@ export const packedNoteDraftSchema = {
|
||||||
},
|
},
|
||||||
replyId: {
|
replyId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
example: 'xxxxxxxxxx',
|
|
||||||
},
|
},
|
||||||
renoteId: {
|
renoteId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
example: 'xxxxxxxxxx',
|
|
||||||
},
|
},
|
||||||
reply: {
|
reply: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
ref: 'Note',
|
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: {
|
renote: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
ref: 'Note',
|
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: {
|
visibility: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -66,7 +62,7 @@ export const packedNoteDraftSchema = {
|
||||||
},
|
},
|
||||||
visibleUserIds: {
|
visibleUserIds: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -75,7 +71,7 @@ export const packedNoteDraftSchema = {
|
||||||
},
|
},
|
||||||
fileIds: {
|
fileIds: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -93,11 +89,11 @@ export const packedNoteDraftSchema = {
|
||||||
},
|
},
|
||||||
hashtag: {
|
hashtag: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
poll: {
|
poll: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
properties: {
|
properties: {
|
||||||
expiresAt: {
|
expiresAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -124,9 +120,8 @@ export const packedNoteDraftSchema = {
|
||||||
},
|
},
|
||||||
channelId: {
|
channelId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
example: 'xxxxxxxxxx',
|
|
||||||
},
|
},
|
||||||
channel: {
|
channel: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -160,12 +155,20 @@ export const packedNoteDraftSchema = {
|
||||||
},
|
},
|
||||||
localOnly: {
|
localOnly: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
reactionAcceptance: {
|
reactionAcceptance: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null],
|
enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null],
|
||||||
},
|
},
|
||||||
|
scheduledAt: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
isActuallyScheduled: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -207,6 +207,36 @@ export const packedNotificationSchema = {
|
||||||
optional: false, nullable: false,
|
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',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|
|
@ -317,6 +317,10 @@ export const packedRolePoliciesSchema = {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
scheduledNoteLimit: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
watermarkAvailable: {
|
watermarkAvailable: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
@ -609,6 +609,8 @@ export const packedMeDetailedOnlySchema = {
|
||||||
quote: { optional: true, ...notificationRecieveConfig },
|
quote: { optional: true, ...notificationRecieveConfig },
|
||||||
reaction: { optional: true, ...notificationRecieveConfig },
|
reaction: { optional: true, ...notificationRecieveConfig },
|
||||||
pollEnded: { optional: true, ...notificationRecieveConfig },
|
pollEnded: { optional: true, ...notificationRecieveConfig },
|
||||||
|
scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
|
||||||
|
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
|
||||||
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
|
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
|
||||||
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
|
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
|
||||||
roleAssigned: { optional: true, ...notificationRecieveConfig },
|
roleAssigned: { optional: true, ...notificationRecieveConfig },
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { QueueLoggerService } from './QueueLoggerService.js';
|
||||||
import { QueueProcessorService } from './QueueProcessorService.js';
|
import { QueueProcessorService } from './QueueProcessorService.js';
|
||||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||||
|
import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js';
|
||||||
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
||||||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||||
|
@ -79,6 +80,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||||
UserWebhookDeliverProcessorService,
|
UserWebhookDeliverProcessorService,
|
||||||
SystemWebhookDeliverProcessorService,
|
SystemWebhookDeliverProcessorService,
|
||||||
EndedPollNotificationProcessorService,
|
EndedPollNotificationProcessorService,
|
||||||
|
PostScheduledNoteProcessorService,
|
||||||
DeliverProcessorService,
|
DeliverProcessorService,
|
||||||
InboxProcessorService,
|
InboxProcessorService,
|
||||||
AggregateRetentionProcessorService,
|
AggregateRetentionProcessorService,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { CheckModeratorsActivityProcessorService } from '@/queue/processors/Chec
|
||||||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||||
|
import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js';
|
||||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||||
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
||||||
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
|
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
|
||||||
|
@ -85,6 +86,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private relationshipQueueWorker: Bull.Worker;
|
private relationshipQueueWorker: Bull.Worker;
|
||||||
private objectStorageQueueWorker: Bull.Worker;
|
private objectStorageQueueWorker: Bull.Worker;
|
||||||
private endedPollNotificationQueueWorker: Bull.Worker;
|
private endedPollNotificationQueueWorker: Bull.Worker;
|
||||||
|
private postScheduledNoteQueueWorker: Bull.Worker;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
|
@ -94,6 +96,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
|
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
|
||||||
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
|
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
|
||||||
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
||||||
|
private postScheduledNoteProcessorService: PostScheduledNoteProcessorService,
|
||||||
private deliverProcessorService: DeliverProcessorService,
|
private deliverProcessorService: DeliverProcessorService,
|
||||||
private inboxProcessorService: InboxProcessorService,
|
private inboxProcessorService: InboxProcessorService,
|
||||||
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
|
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
|
||||||
|
@ -520,6 +523,21 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#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
|
@bindThis
|
||||||
|
@ -534,6 +552,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
this.relationshipQueueWorker.run(),
|
this.relationshipQueueWorker.run(),
|
||||||
this.objectStorageQueueWorker.run(),
|
this.objectStorageQueueWorker.run(),
|
||||||
this.endedPollNotificationQueueWorker.run(),
|
this.endedPollNotificationQueueWorker.run(),
|
||||||
|
this.postScheduledNoteQueueWorker.run(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -549,6 +568,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
this.relationshipQueueWorker.close(),
|
this.relationshipQueueWorker.close(),
|
||||||
this.objectStorageQueueWorker.close(),
|
this.objectStorageQueueWorker.close(),
|
||||||
this.endedPollNotificationQueueWorker.close(),
|
this.endedPollNotificationQueueWorker.close(),
|
||||||
|
this.postScheduledNoteQueueWorker.close(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ export const QUEUE = {
|
||||||
INBOX: 'inbox',
|
INBOX: 'inbox',
|
||||||
SYSTEM: 'system',
|
SYSTEM: 'system',
|
||||||
ENDED_POLL_NOTIFICATION: 'endedPollNotification',
|
ENDED_POLL_NOTIFICATION: 'endedPollNotification',
|
||||||
|
POST_SCHEDULED_NOTE: 'postScheduledNote',
|
||||||
DB: 'db',
|
DB: 'db',
|
||||||
RELATIONSHIP: 'relationship',
|
RELATIONSHIP: 'relationship',
|
||||||
OBJECT_STORAGE: 'objectStorage',
|
OBJECT_STORAGE: 'objectStorage',
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -109,6 +109,10 @@ export type EndedPollNotificationJobData = {
|
||||||
noteId: MiNote['id'];
|
noteId: MiNote['id'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PostScheduledNoteJobData = {
|
||||||
|
noteDraftId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type SystemWebhookDeliverJobData<T extends SystemWebhookEventType = SystemWebhookEventType> = {
|
export type SystemWebhookDeliverJobData<T extends SystemWebhookEventType = SystemWebhookEventType> = {
|
||||||
type: T;
|
type: T;
|
||||||
content: SystemWebhookPayload<T>;
|
content: SystemWebhookPayload<T>;
|
||||||
|
|
|
@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async launch(): Promise<void> {
|
public async launch(): Promise<void> {
|
||||||
const fastify = Fastify({
|
const fastify = Fastify({
|
||||||
trustProxy: true,
|
trustProxy: this.config.trustProxy ?? true,
|
||||||
logger: false,
|
logger: false,
|
||||||
});
|
});
|
||||||
this.#fastify = fastify;
|
this.#fastify = fastify;
|
||||||
|
|
|
@ -34,13 +34,22 @@ export const meta = {
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'MeDetailed',
|
allOf: [
|
||||||
properties: {
|
{
|
||||||
token: {
|
type: 'object',
|
||||||
type: 'string',
|
ref: 'MeDetailed',
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
token: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ export const paramDef = {
|
||||||
startsAt: { type: 'integer' },
|
startsAt: { type: 'integer' },
|
||||||
imageUrl: { type: 'string', minLength: 1 },
|
imageUrl: { type: 'string', minLength: 1 },
|
||||||
dayOfWeek: { type: 'integer' },
|
dayOfWeek: { type: 'integer' },
|
||||||
|
isSensitive: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
|
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -55,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
expiresAt: new Date(ps.expiresAt),
|
expiresAt: new Date(ps.expiresAt),
|
||||||
startsAt: new Date(ps.startsAt),
|
startsAt: new Date(ps.startsAt),
|
||||||
dayOfWeek: ps.dayOfWeek,
|
dayOfWeek: ps.dayOfWeek,
|
||||||
|
isSensitive: ps.isSensitive,
|
||||||
url: ps.url,
|
url: ps.url,
|
||||||
imageUrl: ps.imageUrl,
|
imageUrl: ps.imageUrl,
|
||||||
priority: ps.priority,
|
priority: ps.priority,
|
||||||
|
@ -73,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
expiresAt: ad.expiresAt.toISOString(),
|
expiresAt: ad.expiresAt.toISOString(),
|
||||||
startsAt: ad.startsAt.toISOString(),
|
startsAt: ad.startsAt.toISOString(),
|
||||||
dayOfWeek: ad.dayOfWeek,
|
dayOfWeek: ad.dayOfWeek,
|
||||||
|
isSensitive: ad.isSensitive,
|
||||||
url: ad.url,
|
url: ad.url,
|
||||||
imageUrl: ad.imageUrl,
|
imageUrl: ad.imageUrl,
|
||||||
priority: ad.priority,
|
priority: ad.priority,
|
||||||
|
|
|
@ -63,6 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
expiresAt: ad.expiresAt.toISOString(),
|
expiresAt: ad.expiresAt.toISOString(),
|
||||||
startsAt: ad.startsAt.toISOString(),
|
startsAt: ad.startsAt.toISOString(),
|
||||||
dayOfWeek: ad.dayOfWeek,
|
dayOfWeek: ad.dayOfWeek,
|
||||||
|
isSensitive: ad.isSensitive,
|
||||||
url: ad.url,
|
url: ad.url,
|
||||||
imageUrl: ad.imageUrl,
|
imageUrl: ad.imageUrl,
|
||||||
memo: ad.memo,
|
memo: ad.memo,
|
||||||
|
|
|
@ -39,6 +39,7 @@ export const paramDef = {
|
||||||
expiresAt: { type: 'integer' },
|
expiresAt: { type: 'integer' },
|
||||||
startsAt: { type: 'integer' },
|
startsAt: { type: 'integer' },
|
||||||
dayOfWeek: { type: 'integer' },
|
dayOfWeek: { type: 'integer' },
|
||||||
|
isSensitive: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
required: ['id'],
|
required: ['id'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
|
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
|
||||||
startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
|
startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
|
||||||
dayOfWeek: ps.dayOfWeek,
|
dayOfWeek: ps.dayOfWeek,
|
||||||
|
isSensitive: ps.isSensitive,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });
|
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
|
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, PostScheduledNoteQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -49,6 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||||
|
@Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
|
||||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||||
@Inject('queue:db') public dbQueue: DbQueue,
|
@Inject('queue:db') public dbQueue: DbQueue,
|
||||||
|
|
|
@ -103,6 +103,8 @@ export const meta = {
|
||||||
quote: { optional: true, ...notificationRecieveConfig },
|
quote: { optional: true, ...notificationRecieveConfig },
|
||||||
reaction: { optional: true, ...notificationRecieveConfig },
|
reaction: { optional: true, ...notificationRecieveConfig },
|
||||||
pollEnded: { optional: true, ...notificationRecieveConfig },
|
pollEnded: { optional: true, ...notificationRecieveConfig },
|
||||||
|
scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
|
||||||
|
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
|
||||||
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
|
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
|
||||||
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
|
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
|
||||||
roleAssigned: { optional: true, ...notificationRecieveConfig },
|
roleAssigned: { optional: true, ...notificationRecieveConfig },
|
||||||
|
|
|
@ -18,9 +18,9 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { ApiError } from '../../error.js';
|
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
|
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['federation'],
|
tags: ['federation'],
|
||||||
|
|
|
@ -209,6 +209,8 @@ export const paramDef = {
|
||||||
quote: notificationRecieveConfig,
|
quote: notificationRecieveConfig,
|
||||||
reaction: notificationRecieveConfig,
|
reaction: notificationRecieveConfig,
|
||||||
pollEnded: notificationRecieveConfig,
|
pollEnded: notificationRecieveConfig,
|
||||||
|
scheduledNotePosted: notificationRecieveConfig,
|
||||||
|
scheduledNotePostFailed: notificationRecieveConfig,
|
||||||
receiveFollowRequest: notificationRecieveConfig,
|
receiveFollowRequest: notificationRecieveConfig,
|
||||||
followRequestAccepted: notificationRecieveConfig,
|
followRequestAccepted: notificationRecieveConfig,
|
||||||
roleAssigned: notificationRecieveConfig,
|
roleAssigned: notificationRecieveConfig,
|
||||||
|
|
|
@ -6,17 +6,10 @@
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
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 { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { NoteCreateService } from '@/core/NoteCreateService.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 { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
@ -223,168 +216,28 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
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 noteEntityService: NoteEntityService,
|
||||||
private noteCreateService: NoteCreateService,
|
private noteCreateService: NoteCreateService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
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 {
|
try {
|
||||||
const note = await this.noteCreateService.create(me, {
|
const note = await this.noteCreateService.fetchAndCreate(me, {
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
files: files,
|
fileIds: ps.fileIds ?? ps.mediaIds ?? [],
|
||||||
poll: ps.poll ? {
|
poll: ps.poll ? {
|
||||||
choices: ps.poll.choices,
|
choices: ps.poll.choices,
|
||||||
multiple: ps.poll.multiple ?? false,
|
multiple: ps.poll.multiple ?? false,
|
||||||
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
expiresAt: ps.poll.expiredAfter ? new Date(Date.now() + ps.poll.expiredAfter) : ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
||||||
} : undefined,
|
} : null,
|
||||||
text: ps.text ?? undefined,
|
text: ps.text ?? null,
|
||||||
reply,
|
replyId: ps.replyId ?? null,
|
||||||
renote,
|
renoteId: ps.renoteId ?? null,
|
||||||
cw: ps.cw,
|
cw: ps.cw ?? null,
|
||||||
localOnly: ps.localOnly,
|
localOnly: ps.localOnly,
|
||||||
reactionAcceptance: ps.reactionAcceptance,
|
reactionAcceptance: ps.reactionAcceptance,
|
||||||
visibility: ps.visibility,
|
visibility: ps.visibility,
|
||||||
visibleUsers,
|
visibleUserIds: ps.visibleUserIds ?? [],
|
||||||
channel,
|
channelId: ps.channelId ?? null,
|
||||||
apMentions: ps.noExtractMentions ? [] : undefined,
|
apMentions: ps.noExtractMentions ? [] : undefined,
|
||||||
apHashtags: ps.noExtractHashtags ? [] : undefined,
|
apHashtags: ps.noExtractHashtags ? [] : undefined,
|
||||||
apEmojis: ps.noExtractEmojis ? [] : undefined,
|
apEmojis: ps.noExtractEmojis ? [] : undefined,
|
||||||
|
@ -393,16 +246,46 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return {
|
return {
|
||||||
createdNote: await this.noteEntityService.pack(note, me),
|
createdNote: await this.noteEntityService.pack(note, me),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
|
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
|
||||||
if (e instanceof IdentifiableError) {
|
if (err instanceof IdentifiableError) {
|
||||||
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
|
if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
|
||||||
throw new ApiError(meta.errors.containsProhibitedWords);
|
throw new ApiError(meta.errors.containsProhibitedWords);
|
||||||
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
|
} else if (err.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
|
||||||
throw new ApiError(meta.errors.containsTooManyMentions);
|
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 e;
|
throw err;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,6 +124,12 @@ export const meta = {
|
||||||
id: '9ee33bbe-fde3-4c71-9b51-e50492c6b9c8',
|
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: {
|
cannotRenoteToExternal: {
|
||||||
message: 'Cannot Renote to External.',
|
message: 'Cannot Renote to External.',
|
||||||
code: 'CANNOT_RENOTE_TO_EXTERNAL',
|
code: 'CANNOT_RENOTE_TO_EXTERNAL',
|
||||||
|
@ -162,7 +168,7 @@ export const paramDef = {
|
||||||
fileIds: {
|
fileIds: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
uniqueItems: true,
|
uniqueItems: true,
|
||||||
minItems: 1,
|
minItems: 0,
|
||||||
maxItems: 16,
|
maxItems: 16,
|
||||||
items: { type: 'string', format: 'misskey:id' },
|
items: { type: 'string', format: 'misskey:id' },
|
||||||
},
|
},
|
||||||
|
@ -183,8 +189,10 @@ export const paramDef = {
|
||||||
},
|
},
|
||||||
required: ['choices'],
|
required: ['choices'],
|
||||||
},
|
},
|
||||||
|
scheduledAt: { type: 'integer', nullable: true },
|
||||||
|
isActuallyScheduled: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
required: [],
|
required: ['visibility', 'visibleUserIds', 'cw', 'hashtag', 'localOnly', 'reactionAcceptance', 'replyId', 'renoteId', 'channelId', 'text', 'fileIds', 'poll', 'scheduledAt', 'isActuallyScheduled'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -196,22 +204,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const draft = await this.noteDraftService.create(me, {
|
const draft = await this.noteDraftService.create(me, {
|
||||||
fileIds: ps.fileIds,
|
fileIds: ps.fileIds,
|
||||||
poll: ps.poll ? {
|
pollChoices: ps.poll?.choices ?? [],
|
||||||
choices: ps.poll.choices,
|
pollMultiple: ps.poll?.multiple ?? false,
|
||||||
multiple: ps.poll.multiple ?? false,
|
pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
||||||
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
pollExpiredAfter: ps.poll?.expiredAfter ?? null,
|
||||||
expiredAfter: ps.poll.expiredAfter ?? null,
|
hasPoll: ps.poll != null,
|
||||||
} : undefined,
|
text: ps.text,
|
||||||
text: ps.text ?? null,
|
replyId: ps.replyId,
|
||||||
replyId: ps.replyId ?? undefined,
|
renoteId: ps.renoteId,
|
||||||
renoteId: ps.renoteId ?? undefined,
|
cw: ps.cw,
|
||||||
cw: ps.cw ?? null,
|
hashtag: ps.hashtag,
|
||||||
...(ps.hashtag ? { hashtag: ps.hashtag } : {}),
|
|
||||||
localOnly: ps.localOnly,
|
localOnly: ps.localOnly,
|
||||||
reactionAcceptance: ps.reactionAcceptance,
|
reactionAcceptance: ps.reactionAcceptance,
|
||||||
visibility: ps.visibility,
|
visibility: ps.visibility,
|
||||||
visibleUserIds: ps.visibleUserIds ?? [],
|
visibleUserIds: ps.visibleUserIds,
|
||||||
channelId: ps.channelId ?? undefined,
|
channelId: ps.channelId,
|
||||||
|
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
|
||||||
|
isActuallyScheduled: ps.isActuallyScheduled,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
if (err instanceof IdentifiableError) {
|
if (err instanceof IdentifiableError) {
|
||||||
switch (err.id) {
|
switch (err.id) {
|
||||||
|
@ -241,6 +250,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
||||||
case '215dbc76-336c-4d2a-9605-95766ba7dab0':
|
case '215dbc76-336c-4d2a-9605-95766ba7dab0':
|
||||||
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
||||||
|
case 'c3275f19-4558-4c59-83e1-4f684b5fab66':
|
||||||
|
throw new ApiError(meta.errors.tooManyScheduledNotes);
|
||||||
default:
|
default:
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ export const paramDef = {
|
||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
sinceDate: { type: 'integer' },
|
sinceDate: { type: 'integer' },
|
||||||
untilDate: { type: 'integer' },
|
untilDate: { type: 'integer' },
|
||||||
|
scheduled: { type: 'boolean', nullable: true },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -58,6 +59,12 @@ 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)
|
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 });
|
.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
|
const drafts = await query
|
||||||
.limit(ps.limit)
|
.limit(ps.limit)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
|
@ -159,6 +159,12 @@ export const meta = {
|
||||||
code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
|
code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
|
||||||
id: '215dbc76-336c-4d2a-9605-95766ba7dab0',
|
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: {
|
limit: {
|
||||||
|
@ -171,14 +177,14 @@ export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
draftId: { type: 'string', nullable: false, format: 'misskey:id' },
|
draftId: { type: 'string', nullable: false, format: 'misskey:id' },
|
||||||
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
|
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'] },
|
||||||
visibleUserIds: { type: 'array', uniqueItems: true, items: {
|
visibleUserIds: { type: 'array', uniqueItems: true, items: {
|
||||||
type: 'string', format: 'misskey:id',
|
type: 'string', format: 'misskey:id',
|
||||||
} },
|
} },
|
||||||
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 },
|
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 },
|
||||||
hashtag: { type: 'string', nullable: true, maxLength: 200 },
|
hashtag: { type: 'string', nullable: true, maxLength: 200 },
|
||||||
localOnly: { type: 'boolean', default: false },
|
localOnly: { type: 'boolean' },
|
||||||
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
|
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'] },
|
||||||
replyId: { type: 'string', format: 'misskey:id', nullable: true },
|
replyId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
|
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
channelId: { type: 'string', format: 'misskey:id', nullable: true },
|
channelId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
|
@ -194,7 +200,7 @@ export const paramDef = {
|
||||||
fileIds: {
|
fileIds: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
uniqueItems: true,
|
uniqueItems: true,
|
||||||
minItems: 1,
|
minItems: 0,
|
||||||
maxItems: 16,
|
maxItems: 16,
|
||||||
items: { type: 'string', format: 'misskey:id' },
|
items: { type: 'string', format: 'misskey:id' },
|
||||||
},
|
},
|
||||||
|
@ -215,6 +221,8 @@ export const paramDef = {
|
||||||
},
|
},
|
||||||
required: ['choices'],
|
required: ['choices'],
|
||||||
},
|
},
|
||||||
|
scheduledAt: { type: 'integer', nullable: true },
|
||||||
|
isActuallyScheduled: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
required: ['draftId'],
|
required: ['draftId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -228,22 +236,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const draft = await this.noteDraftService.update(me, ps.draftId, {
|
const draft = await this.noteDraftService.update(me, ps.draftId, {
|
||||||
fileIds: ps.fileIds,
|
fileIds: ps.fileIds,
|
||||||
poll: ps.poll ? {
|
pollChoices: ps.poll?.choices,
|
||||||
choices: ps.poll.choices,
|
pollMultiple: ps.poll?.multiple,
|
||||||
multiple: ps.poll.multiple ?? false,
|
pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
||||||
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
pollExpiredAfter: ps.poll?.expiredAfter,
|
||||||
expiredAfter: ps.poll.expiredAfter ?? null,
|
text: ps.text,
|
||||||
} : undefined,
|
replyId: ps.replyId,
|
||||||
text: ps.text ?? null,
|
renoteId: ps.renoteId,
|
||||||
replyId: ps.replyId ?? undefined,
|
cw: ps.cw,
|
||||||
renoteId: ps.renoteId ?? undefined,
|
hashtag: ps.hashtag,
|
||||||
cw: ps.cw ?? null,
|
|
||||||
...(ps.hashtag ? { hashtag: ps.hashtag } : {}),
|
|
||||||
localOnly: ps.localOnly,
|
localOnly: ps.localOnly,
|
||||||
reactionAcceptance: ps.reactionAcceptance,
|
reactionAcceptance: ps.reactionAcceptance,
|
||||||
visibility: ps.visibility,
|
visibility: ps.visibility,
|
||||||
visibleUserIds: ps.visibleUserIds ?? [],
|
visibleUserIds: ps.visibleUserIds,
|
||||||
channelId: ps.channelId ?? undefined,
|
channelId: ps.channelId,
|
||||||
|
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
|
||||||
|
isActuallyScheduled: ps.isActuallyScheduled,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
if (err instanceof IdentifiableError) {
|
if (err instanceof IdentifiableError) {
|
||||||
switch (err.id) {
|
switch (err.id) {
|
||||||
|
@ -285,6 +293,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.containsProhibitedWords);
|
throw new ApiError(meta.errors.containsProhibitedWords);
|
||||||
case '4de0363a-3046-481b-9b0f-feff3e211025':
|
case '4de0363a-3046-481b-9b0f-feff3e211025':
|
||||||
throw new ApiError(meta.errors.containsTooManyMentions);
|
throw new ApiError(meta.errors.containsTooManyMentions);
|
||||||
|
case 'bacdf856-5c51-4159-b88a-804fa5103be5':
|
||||||
|
throw new ApiError(meta.errors.tooManyScheduledNotes);
|
||||||
default:
|
default:
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,10 +29,16 @@ export const meta = {
|
||||||
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
|
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
|
||||||
},
|
},
|
||||||
|
|
||||||
signinRequired: {
|
contentRestrictedByUser: {
|
||||||
message: 'Signin required.',
|
message: 'Content restricted by user. Please sign in to view.',
|
||||||
code: 'SIGNIN_REQUIRED',
|
code: 'CONTENT_RESTRICTED_BY_USER',
|
||||||
id: '8e75455b-738c-471d-9f80-62693f33372e',
|
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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -61,15 +67,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
});
|
});
|
||||||
|
|
||||||
if (note.user!.requireSigninToViewContents && me == null) {
|
if (note.user!.requireSigninToViewContents && me == null) {
|
||||||
throw new ApiError(meta.errors.signinRequired);
|
throw new ApiError(meta.errors.contentRestrictedByUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) {
|
if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) {
|
||||||
throw new ApiError(meta.errors.signinRequired);
|
throw new ApiError(meta.errors.contentRestrictedByServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) {
|
if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) {
|
||||||
throw new ApiError(meta.errors.signinRequired);
|
throw new ApiError(meta.errors.contentRestrictedByServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.noteEntityService.pack(note, me, {
|
return await this.noteEntityService.pack(note, me, {
|
||||||
|
|
|
@ -22,17 +22,26 @@ export const meta = {
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'UserList',
|
allOf: [
|
||||||
properties: {
|
{
|
||||||
likedCount: {
|
type: 'object',
|
||||||
type: 'number',
|
ref: 'UserList',
|
||||||
optional: true, nullable: false,
|
|
||||||
},
|
},
|
||||||
isLiked: {
|
{
|
||||||
type: 'boolean',
|
type: 'object',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
likedCount: {
|
||||||
|
type: 'number',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
|
isLiked: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
};
|
};
|
||||||
window.onunhandledrejection = (e) => {
|
window.onunhandledrejection = (e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
|
renderError('SOMETHING_HAPPENED_IN_PROMISE', e.reason || e);
|
||||||
};
|
};
|
||||||
|
|
||||||
let forceError = localStorage.getItem('forceError');
|
let forceError = localStorage.getItem('forceError');
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
* quote - 投稿が引用Renoteされた
|
* quote - 投稿が引用Renoteされた
|
||||||
* reaction - 投稿にリアクションされた
|
* reaction - 投稿にリアクションされた
|
||||||
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
||||||
|
* scheduledNotePosted - 予約したノートが投稿された
|
||||||
|
* scheduledNotePostFailed - 予約したノートの投稿に失敗した
|
||||||
* receiveFollowRequest - フォローリクエストされた
|
* receiveFollowRequest - フォローリクエストされた
|
||||||
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
||||||
* roleAssigned - ロールが付与された
|
* roleAssigned - ロールが付与された
|
||||||
|
@ -32,6 +34,8 @@ export const notificationTypes = [
|
||||||
'quote',
|
'quote',
|
||||||
'reaction',
|
'reaction',
|
||||||
'pollEnded',
|
'pollEnded',
|
||||||
|
'scheduledNotePosted',
|
||||||
|
'scheduledNotePostFailed',
|
||||||
'receiveFollowRequest',
|
'receiveFollowRequest',
|
||||||
'followRequestAccepted',
|
'followRequestAccepted',
|
||||||
'roleAssigned',
|
'roleAssigned',
|
||||||
|
|
|
@ -68,7 +68,6 @@ async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse
|
||||||
return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
|
return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
|
||||||
ADMIN_CACHE.set(host, {
|
ADMIN_CACHE.set(host, {
|
||||||
id: res.id,
|
id: res.id,
|
||||||
// @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this
|
|
||||||
i: res.token,
|
i: res.token,
|
||||||
});
|
});
|
||||||
return res as Misskey.entities.SignupResponse;
|
return res as Misskey.entities.SignupResponse;
|
||||||
|
|
|
@ -20,6 +20,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"estree-walker": "3.0.3",
|
"estree-walker": "3.0.3",
|
||||||
"magic-string": "0.30.17",
|
"magic-string": "0.30.17",
|
||||||
"vite": "7.0.6"
|
"vite": "7.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,9 +46,71 @@ export default [
|
||||||
allowSingleExtends: true,
|
allowSingleExtends: true,
|
||||||
}],
|
}],
|
||||||
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||||
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||||
'id-denylist': ['error', 'window', 'e'],
|
// 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 という変数名を定義し忘れている',
|
||||||
|
},
|
||||||
|
],
|
||||||
'no-shadow': ['warn'],
|
'no-shadow': ['warn'],
|
||||||
'vue/attributes-order': ['error', {
|
'vue/attributes-order': ['error', {
|
||||||
alphabetical: false,
|
alphabetical: false,
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
"uuid": "11.1.0",
|
"uuid": "11.1.0",
|
||||||
"vite": "7.1.4",
|
"vite": "7.1.5",
|
||||||
"vue": "3.5.21"
|
"vue": "3.5.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -33,7 +33,7 @@ import type { Theme } from '@/theme.js';
|
||||||
console.log('Misskey Embed');
|
console.log('Misskey Embed');
|
||||||
|
|
||||||
//#region Embedパラメータの取得・パース
|
//#region Embedパラメータの取得・パース
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const embedParams = parseEmbedParams(params);
|
const embedParams = parseEmbedParams(params);
|
||||||
if (_DEV_) console.log(embedParams);
|
if (_DEV_) console.log(embedParams);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -81,7 +81,7 @@ storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload });
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// サイズの制限
|
// サイズの制限
|
||||||
document.documentElement.style.maxWidth = '500px';
|
window.document.documentElement.style.maxWidth = '500px';
|
||||||
|
|
||||||
// iframeIdの設定
|
// iframeIdの設定
|
||||||
function setIframeIdHandler(event: MessageEvent) {
|
function setIframeIdHandler(event: MessageEvent) {
|
||||||
|
@ -114,16 +114,16 @@ app.provide(DI.embedParams, embedParams);
|
||||||
const rootEl = ((): HTMLElement => {
|
const rootEl = ((): HTMLElement => {
|
||||||
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
|
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
|
||||||
|
|
||||||
const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
|
const currentRoot = window.document.getElementById(MISSKEY_MOUNT_DIV_ID);
|
||||||
|
|
||||||
if (currentRoot) {
|
if (currentRoot) {
|
||||||
console.warn('multiple import detected');
|
console.warn('multiple import detected');
|
||||||
return currentRoot;
|
return currentRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = document.createElement('div');
|
const root = window.document.createElement('div');
|
||||||
root.id = MISSKEY_MOUNT_DIV_ID;
|
root.id = MISSKEY_MOUNT_DIV_ID;
|
||||||
document.body.appendChild(root);
|
window.document.body.appendChild(root);
|
||||||
return root;
|
return root;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hu
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
function removeSplash() {
|
function removeSplash() {
|
||||||
const splash = document.getElementById('splash');
|
const splash = window.document.getElementById('splash');
|
||||||
if (splash) {
|
if (splash) {
|
||||||
splash.style.opacity = '0';
|
splash.style.opacity = '0';
|
||||||
splash.style.pointerEvents = 'none';
|
splash.style.pointerEvents = 'none';
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha
|
||||||
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
|
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
|
||||||
// テスト環境で Web Worker インスタンスは作成できない
|
// テスト環境で Web Worker インスタンスは作成できない
|
||||||
if (import.meta.env.MODE === 'test') {
|
if (import.meta.env.MODE === 'test') {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = window.document.createElement('canvas');
|
||||||
canvas.width = 64;
|
canvas.width = 64;
|
||||||
canvas.height = 64;
|
canvas.height = 64;
|
||||||
resolve(canvas);
|
resolve(canvas);
|
||||||
|
@ -34,7 +34,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
|
||||||
);
|
);
|
||||||
resolve(workers);
|
resolve(workers);
|
||||||
} else {
|
} else {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = window.document.createElement('canvas');
|
||||||
canvas.width = 64;
|
canvas.width = 64;
|
||||||
canvas.height = 64;
|
canvas.height = 64;
|
||||||
resolve(canvas);
|
resolve(canvas);
|
||||||
|
|
|
@ -29,7 +29,7 @@ const props = defineProps<{
|
||||||
// if no instance data is given, this is for the local instance
|
// if no instance data is given, this is for the local instance
|
||||||
const instance = props.instance ?? {
|
const instance = props.instance ?? {
|
||||||
name: serverMetadata.name,
|
name: serverMetadata.name,
|
||||||
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content,
|
themeColor: (window.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');
|
const faviconUrl = computed(() => props.instance ? mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : mediaProxy.getProxiedImageUrlNullable(serverMetadata.iconUrl, 'preview') ?? '/favicon.ico');
|
||||||
|
|
|
@ -27,7 +27,7 @@ const canonical = props.host === localHost ? `@${props.username}` : `@${props.us
|
||||||
|
|
||||||
const url = `/${canonical}`;
|
const url = `/${canonical}`;
|
||||||
|
|
||||||
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-mention'));
|
const bg = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-mention'));
|
||||||
bg.setAlpha(0.1);
|
bg.setAlpha(0.1);
|
||||||
const bgCss = bg.toRgbString();
|
const bgCss = bg.toRgbString();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -134,7 +134,7 @@ const isBackTop = ref(false);
|
||||||
const empty = computed(() => items.value.size === 0);
|
const empty = computed(() => items.value.size === 0);
|
||||||
const error = ref(false);
|
const error = ref(false);
|
||||||
|
|
||||||
const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : document.body);
|
const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : window.document.body);
|
||||||
|
|
||||||
const visibility = useDocumentVisibility();
|
const visibility = useDocumentVisibility();
|
||||||
|
|
||||||
|
@ -353,7 +353,7 @@ watch(visibility, () => {
|
||||||
BACKGROUND_PAUSE_WAIT_SEC * 1000);
|
BACKGROUND_PAUSE_WAIT_SEC * 1000);
|
||||||
} else { // 'visible'
|
} else { // 'visible'
|
||||||
if (timerForSetPause) {
|
if (timerForSetPause) {
|
||||||
clearTimeout(timerForSetPause);
|
window.clearTimeout(timerForSetPause);
|
||||||
timerForSetPause = null;
|
timerForSetPause = null;
|
||||||
} else {
|
} else {
|
||||||
isPausingUpdate = false;
|
isPausingUpdate = false;
|
||||||
|
@ -447,11 +447,11 @@ onBeforeMount(() => {
|
||||||
init().then(() => {
|
init().then(() => {
|
||||||
if (props.pagination.reversed) {
|
if (props.pagination.reversed) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(toBottom, 800);
|
window.setTimeout(toBottom, 800);
|
||||||
|
|
||||||
// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
|
// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
|
||||||
// more = trueを遅らせる
|
// more = trueを遅らせる
|
||||||
setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
|
@ -461,11 +461,11 @@ onBeforeMount(() => {
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (timerForSetPause) {
|
if (timerForSetPause) {
|
||||||
clearTimeout(timerForSetPause);
|
window.clearTimeout(timerForSetPause);
|
||||||
timerForSetPause = null;
|
timerForSetPause = null;
|
||||||
}
|
}
|
||||||
if (preventAppearFetchMoreTimer.value) {
|
if (preventAppearFetchMoreTimer.value) {
|
||||||
clearTimeout(preventAppearFetchMoreTimer.value);
|
window.clearTimeout(preventAppearFetchMoreTimer.value);
|
||||||
preventAppearFetchMoreTimer.value = null;
|
preventAppearFetchMoreTimer.value = null;
|
||||||
}
|
}
|
||||||
scrollObserver.value?.disconnect();
|
scrollObserver.value?.disconnect();
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
const providedContextEl = document.getElementById('misskey_embedCtx');
|
const providedContextEl = window.document.getElementById('misskey_embedCtx');
|
||||||
|
|
||||||
export type ServerContext = {
|
export type ServerContext = {
|
||||||
clip?: Misskey.entities.Clip;
|
clip?: Misskey.entities.Clip;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { misskeyApi } from '@/misskey-api.js';
|
import { misskeyApi } from '@/misskey-api.js';
|
||||||
|
|
||||||
const providedMetaEl = document.getElementById('misskey_meta');
|
const providedMetaEl = window.document.getElementById('misskey_meta');
|
||||||
|
|
||||||
const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null;
|
const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null;
|
||||||
|
|
||||||
|
|
|
@ -35,15 +35,15 @@ export function assertIsTheme(theme: Record<string, unknown>): theme is Theme {
|
||||||
export function applyTheme(theme: Theme, persist = true) {
|
export function applyTheme(theme: Theme, persist = true) {
|
||||||
if (timeout) window.clearTimeout(timeout);
|
if (timeout) window.clearTimeout(timeout);
|
||||||
|
|
||||||
document.documentElement.classList.add('_themeChanging_');
|
window.document.documentElement.classList.add('_themeChanging_');
|
||||||
|
|
||||||
timeout = window.setTimeout(() => {
|
timeout = window.setTimeout(() => {
|
||||||
document.documentElement.classList.remove('_themeChanging_');
|
window.document.documentElement.classList.remove('_themeChanging_');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
|
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
|
||||||
|
|
||||||
document.documentElement.dataset.colorScheme = colorScheme;
|
window.document.documentElement.dataset.colorScheme = colorScheme;
|
||||||
|
|
||||||
// Deep copy
|
// Deep copy
|
||||||
const _theme = JSON.parse(JSON.stringify(theme));
|
const _theme = JSON.parse(JSON.stringify(theme));
|
||||||
|
@ -55,7 +55,7 @@ export function applyTheme(theme: Theme, persist = true) {
|
||||||
|
|
||||||
const props = compile(_theme);
|
const props = compile(_theme);
|
||||||
|
|
||||||
for (const tag of document.head.children) {
|
for (const tag of window.document.head.children) {
|
||||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||||
tag.setAttribute('content', props['htmlThemeColor']);
|
tag.setAttribute('content', props['htmlThemeColor']);
|
||||||
break;
|
break;
|
||||||
|
@ -63,7 +63,7 @@ export function applyTheme(theme: Theme, persist = true) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(props)) {
|
for (const [k, v] of Object.entries(props)) {
|
||||||
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
|
window.document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照
|
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照
|
||||||
|
|
|
@ -52,8 +52,8 @@ function safeURIDecode(str: string): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = location.pathname.split('/')[2];
|
const page = window.location.pathname.split('/')[2];
|
||||||
const contentId = safeURIDecode(location.pathname.split('/')[3]);
|
const contentId = safeURIDecode(window.location.pathname.split('/')[3]);
|
||||||
if (_DEV_) console.log(page, contentId);
|
if (_DEV_) console.log(page, contentId);
|
||||||
|
|
||||||
const embedParams = inject(DI.embedParams, defaultEmbedParams);
|
const embedParams = inject(DI.embedParams, defaultEmbedParams);
|
||||||
|
|
|
@ -64,6 +64,8 @@ function toBase62(n: number): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConfig(): UserConfig {
|
export function getConfig(): UserConfig {
|
||||||
|
const localesHash = toBase62(hash(JSON.stringify(locales)));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base: '/embed_vite/',
|
base: '/embed_vite/',
|
||||||
|
|
||||||
|
@ -148,9 +150,9 @@ export function getConfig(): UserConfig {
|
||||||
// dependencies of i18n.ts
|
// dependencies of i18n.ts
|
||||||
'config': ['@@/js/config.js'],
|
'config': ['@@/js/config.js'],
|
||||||
},
|
},
|
||||||
entryFileNames: 'scripts/[hash:8].js',
|
entryFileNames: `scripts/${localesHash}-[hash:8].js`,
|
||||||
chunkFileNames: 'scripts/[hash:8].js',
|
chunkFileNames: `scripts/${localesHash}-[hash:8].js`,
|
||||||
assetFileNames: 'assets/[hash:8][extname]',
|
assetFileNames: `assets/${localesHash}-[hash:8][extname]`,
|
||||||
paths(id) {
|
paths(id) {
|
||||||
for (const p of externalPackages) {
|
for (const p of externalPackages) {
|
||||||
if (p.match.test(id)) {
|
if (p.match.test(id)) {
|
||||||
|
|
|
@ -51,9 +51,71 @@ export default [
|
||||||
allowSingleExtends: true,
|
allowSingleExtends: true,
|
||||||
}],
|
}],
|
||||||
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||||
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||||
'id-denylist': ['error', 'window', 'e'],
|
// 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 という変数名を定義し忘れている',
|
||||||
|
},
|
||||||
|
],
|
||||||
'no-shadow': ['warn'],
|
'no-shadow': ['warn'],
|
||||||
'vue/attributes-order': ['error', {
|
'vue/attributes-order': ['error', {
|
||||||
alphabetical: false,
|
alphabetical: false,
|
||||||
|
|
|
@ -4,15 +4,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
|
const address = new URL(window.document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || window.location.href);
|
||||||
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
|
const siteName = window.document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
|
||||||
|
|
||||||
export const host = address.host;
|
export const host = address.host;
|
||||||
export const hostname = address.hostname;
|
export const hostname = address.hostname;
|
||||||
export const url = address.origin;
|
export const url = address.origin;
|
||||||
export const port = address.port;
|
export const port = address.port;
|
||||||
export const apiUrl = location.origin + '/api';
|
export const apiUrl = window.location.origin + '/api';
|
||||||
export const wsOrigin = location.origin;
|
export const wsOrigin = window.location.origin;
|
||||||
export const lang = localStorage.getItem('lang') ?? 'en-US';
|
export const lang = localStorage.getItem('lang') ?? 'en-US';
|
||||||
export const langs = _LANGS_;
|
export const langs = _LANGS_;
|
||||||
export const version = _VERSION_;
|
export const version = _VERSION_;
|
||||||
|
|
|
@ -51,7 +51,7 @@ export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknow
|
||||||
// - toleranceの範囲内に収まる程度の微量なスクロールが発生した
|
// - toleranceの範囲内に収まる程度の微量なスクロールが発生した
|
||||||
let prevTopVisible = firstTopVisible;
|
let prevTopVisible = firstTopVisible;
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
if (!document.body.contains(el)) return;
|
if (!window.document.body.contains(el)) return;
|
||||||
|
|
||||||
const topVisible = isHeadVisible(el, tolerance);
|
const topVisible = isHeadVisible(el, tolerance);
|
||||||
if (topVisible !== prevTopVisible) {
|
if (topVisible !== prevTopVisible) {
|
||||||
|
@ -78,7 +78,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
|
||||||
|
|
||||||
const containerOrWindow = container ?? window;
|
const containerOrWindow = container ?? window;
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
if (!document.body.contains(el)) return;
|
if (!window.document.body.contains(el)) return;
|
||||||
if (isTailVisible(el, 1, container)) {
|
if (isTailVisible(el, 1, container)) {
|
||||||
cb();
|
cb();
|
||||||
if (once) removeListener();
|
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
|
// https://ja.javascript.info/size-and-scroll-window#ref-932
|
||||||
export function getBodyScrollHeight() {
|
export function getBodyScrollHeight() {
|
||||||
return Math.max(
|
return Math.max(
|
||||||
document.body.scrollHeight, document.documentElement.scrollHeight,
|
window.document.body.scrollHeight, window.document.documentElement.scrollHeight,
|
||||||
document.body.offsetHeight, document.documentElement.offsetHeight,
|
window.document.body.offsetHeight, window.document.documentElement.offsetHeight,
|
||||||
document.body.clientHeight, document.documentElement.clientHeight,
|
window.document.body.clientHeight, window.document.documentElement.clientHeight,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,18 +7,18 @@ import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
|
export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
|
||||||
const visibility = ref(document.visibilityState);
|
const visibility = ref(window.document.visibilityState);
|
||||||
|
|
||||||
const onChange = (): void => {
|
const onChange = (): void => {
|
||||||
visibility.value = document.visibilityState;
|
visibility.value = window.document.visibilityState;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('visibilitychange', onChange);
|
window.document.addEventListener('visibilitychange', onChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('visibilitychange', onChange);
|
window.document.removeEventListener('visibilitychange', onChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
return visibility;
|
return visibility;
|
||||||
|
|
|
@ -57,12 +57,15 @@
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"magic-string": "0.30.18",
|
"magic-string": "0.30.18",
|
||||||
"matter-js": "0.20.0",
|
"matter-js": "0.20.0",
|
||||||
|
"mediabunny": "1.15.1",
|
||||||
"mfm-js": "0.25.0",
|
"mfm-js": "0.25.0",
|
||||||
"misskey-bubble-game": "workspace:*",
|
"misskey-bubble-game": "workspace:*",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
"misskey-reversi": "workspace:*",
|
"misskey-reversi": "workspace:*",
|
||||||
"photoswipe": "5.4.4",
|
"photoswipe": "5.4.4",
|
||||||
"punycode.js": "2.3.1",
|
"punycode.js": "2.3.1",
|
||||||
|
"qr-code-styling": "1.9.2",
|
||||||
|
"qr-scanner": "1.4.2",
|
||||||
"rollup": "4.50.1",
|
"rollup": "4.50.1",
|
||||||
"sanitize-html": "2.17.0",
|
"sanitize-html": "2.17.0",
|
||||||
"sass": "1.92.1",
|
"sass": "1.92.1",
|
||||||
|
@ -76,7 +79,7 @@
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
"v-code-diff": "1.13.1",
|
"v-code-diff": "1.13.1",
|
||||||
"vite": "7.1.4",
|
"vite": "7.1.5",
|
||||||
"vue": "3.5.21",
|
"vue": "3.5.21",
|
||||||
"vuedraggable": "next",
|
"vuedraggable": "next",
|
||||||
"wanakana": "5.3.1"
|
"wanakana": "5.3.1"
|
||||||
|
|
|
@ -151,7 +151,21 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
||||||
}
|
}
|
||||||
//#endregion
|
//#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: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
|
||||||
|
// NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため)
|
||||||
|
// see: https://github.com/misskey-dev/misskey/issues/16562
|
||||||
watch(store.r.darkMode, (darkMode) => {
|
watch(store.r.darkMode, (darkMode) => {
|
||||||
const theme = (() => {
|
const theme = (() => {
|
||||||
if (darkMode) {
|
if (darkMode) {
|
||||||
|
@ -183,18 +197,6 @@ 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 (!isSafeMode) {
|
||||||
if (prefer.s.darkTheme && store.s.darkMode) {
|
if (prefer.s.darkTheme && store.s.darkMode) {
|
||||||
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
|
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
|
||||||
|
|
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
|
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
|
||||||
import isChromatic from 'chromatic/isChromatic';
|
import isChromatic from 'chromatic/isChromatic';
|
||||||
|
import { initShaderProgram } from '@/utility/webgl.js';
|
||||||
|
|
||||||
const canvasEl = useTemplateRef('canvasEl');
|
const canvasEl = useTemplateRef('canvasEl');
|
||||||
|
|
||||||
|
@ -21,47 +22,6 @@ const props = withDefaults(defineProps<{
|
||||||
focus: 1.0,
|
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;
|
let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -71,7 +31,7 @@ onMounted(() => {
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
|
|
||||||
const maybeGl = canvas.getContext('webgl', { premultipliedAlpha: true });
|
const maybeGl = canvas.getContext('webgl2', { premultipliedAlpha: true });
|
||||||
if (maybeGl == null) return;
|
if (maybeGl == null) return;
|
||||||
|
|
||||||
const gl = maybeGl;
|
const gl = maybeGl;
|
||||||
|
@ -82,18 +42,16 @@ onMounted(() => {
|
||||||
const positionBuffer = gl.createBuffer();
|
const positionBuffer = gl.createBuffer();
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||||
|
|
||||||
const shaderProgram = initShaderProgram(gl, `
|
const shaderProgram = initShaderProgram(gl, `#version 300 es
|
||||||
attribute vec2 vertex;
|
in vec2 position;
|
||||||
|
|
||||||
uniform vec2 u_scale;
|
uniform vec2 u_scale;
|
||||||
|
out vec2 in_uv;
|
||||||
varying vec2 v_pos;
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
gl_Position = vec4(vertex, 0.0, 1.0);
|
gl_Position = vec4(position, 0.0, 1.0);
|
||||||
v_pos = vertex / u_scale;
|
in_uv = position / u_scale;
|
||||||
}
|
}
|
||||||
`, `
|
`, `#version 300 es
|
||||||
precision mediump float;
|
precision mediump float;
|
||||||
|
|
||||||
vec3 mod289(vec3 x) {
|
vec3 mod289(vec3 x) {
|
||||||
|
@ -143,6 +101,7 @@ onMounted(() => {
|
||||||
return 130.0 * dot(m, g);
|
return 130.0 * dot(m, g);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
in vec2 in_uv;
|
||||||
uniform float u_time;
|
uniform float u_time;
|
||||||
uniform vec2 u_resolution;
|
uniform vec2 u_resolution;
|
||||||
uniform float u_spread;
|
uniform float u_spread;
|
||||||
|
@ -150,8 +109,7 @@ onMounted(() => {
|
||||||
uniform float u_warp;
|
uniform float u_warp;
|
||||||
uniform float u_focus;
|
uniform float u_focus;
|
||||||
uniform float u_itensity;
|
uniform float u_itensity;
|
||||||
|
out vec4 out_color;
|
||||||
varying vec2 v_pos;
|
|
||||||
|
|
||||||
float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
|
float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
|
||||||
float SPREAD = 0.7 * u_spread;
|
float SPREAD = 0.7 * u_spread;
|
||||||
|
@ -182,13 +140,13 @@ onMounted(() => {
|
||||||
|
|
||||||
float ratio = u_resolution.x / u_resolution.y;
|
float ratio = u_resolution.x / u_resolution.y;
|
||||||
|
|
||||||
vec2 uv = vec2( v_pos.x, v_pos.y / ratio ) * 0.5 + 0.5;
|
vec2 uv = vec2( in_uv.x, in_uv.y / ratio ) * 0.5 + 0.5;
|
||||||
|
|
||||||
vec3 color = vec3( 0.0 );
|
vec3 color = vec3( 0.0 );
|
||||||
|
|
||||||
float greenMix = snoise( v_pos * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
|
float greenMix = snoise( in_uv * 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 purpleMix = snoise( in_uv * 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 orangeMix = snoise( in_uv * 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 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 );
|
float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 );
|
||||||
|
@ -198,10 +156,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.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 += 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( v_pos.y + u_time * 0.00013, v_pos.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
|
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 );
|
||||||
|
|
||||||
vec3 inverted = vec3( 1.0 ) - color;
|
vec3 inverted = vec3( 1.0 ) - color;
|
||||||
gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
|
out_color = vec4(color, max(max(color.x, color.y), color.z));
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
if (shaderProgram == null) return;
|
if (shaderProgram == null) return;
|
||||||
|
@ -223,7 +181,7 @@ onMounted(() => {
|
||||||
gl.uniform1f(u_itensity, 0.5);
|
gl.uniform1f(u_itensity, 0.5);
|
||||||
gl.uniform2fv(u_scale, [props.scale, props.scale]);
|
gl.uniform2fv(u_scale, [props.scale, props.scale]);
|
||||||
|
|
||||||
const vertex = gl.getAttribLocation(shaderProgram, 'vertex');
|
const vertex = gl.getAttribLocation(shaderProgram, 'position');
|
||||||
gl.enableVertexAttribArray(vertex);
|
gl.enableVertexAttribArray(vertex);
|
||||||
gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0);
|
gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0);
|
||||||
|
|
||||||
|
|
|
@ -10,17 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInput v-model="name">
|
<MkInput v-model="name">
|
||||||
<template #label>{{ i18n.ts.name }}</template>
|
<template #label>{{ i18n.ts.name }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-model="src">
|
<MkSelect v-model="src" :items="antennaSourcesSelectDef">
|
||||||
<template #label>{{ i18n.ts.antennaSource }}</template>
|
<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>
|
||||||
<MkSelect v-if="src === 'list'" v-model="userListId">
|
<MkSelect v-if="src === 'list'" v-model="userListId" :items="userListsSelectDef">
|
||||||
<template #label>{{ i18n.ts.userList }}</template>
|
<template #label>{{ i18n.ts.userList }}</template>
|
||||||
<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users">
|
<MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users">
|
||||||
<template #label>{{ i18n.ts.users }}</template>
|
<template #label>{{ i18n.ts.users }}</template>
|
||||||
|
@ -52,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { watch, ref } from 'vue';
|
import { watch, ref, computed } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { DeepPartial } from '@/utility/merge.js';
|
import type { DeepPartial } from '@/utility/merge.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
@ -64,6 +58,7 @@ import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { deepMerge } from '@/utility/merge.js';
|
import { deepMerge } from '@/utility/merge.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
|
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -99,9 +94,35 @@ const emit = defineEmits<{
|
||||||
(ev: 'deleted'): void,
|
(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 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 users = ref<string>(initialAntenna.users.join('\n'));
|
||||||
const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n'));
|
const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n'));
|
||||||
const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
|
const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
|
||||||
|
|
|
@ -32,10 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" @update:modelValue="onSelectUpdate">
|
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" :items="selectDef" @update:modelValue="onSelectUpdate">
|
||||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||||
<template v-if="c.caption" #caption>{{ c.caption }}</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>
|
</MkSelect>
|
||||||
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton>
|
<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">
|
<div v-else-if="c.type === 'postForm'" :class="$style.postForm">
|
||||||
|
@ -74,6 +73,7 @@ import MkSelect from '@/components/MkSelect.vue';
|
||||||
import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
|
import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkPostForm from '@/components/MkPostForm.vue';
|
import MkPostForm from '@/components/MkPostForm.vue';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
component: AsUiComponent;
|
component: AsUiComponent;
|
||||||
|
@ -130,7 +130,19 @@ function onSwitchUpdate(v: boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueForSelect = ref('default' in c && typeof c.default !== 'boolean' ? c.default ?? null : null);
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
function onSelectUpdate(v) {
|
function onSelectUpdate(v) {
|
||||||
valueForSelect.value = v;
|
valueForSelect.value = v;
|
||||||
|
|
|
@ -29,6 +29,6 @@ const users = ref<Misskey.entities.UserLite[]>([]);
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
users.value = await misskeyApi('users/show', {
|
users.value = await misskeyApi('users/show', {
|
||||||
userIds: props.userIds,
|
userIds: props.userIds,
|
||||||
}) as unknown as Misskey.entities.UserLite[];
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -29,16 +29,7 @@ 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' })"/>
|
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
|
||||||
</template>
|
</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-if="select" v-model="selectedValue" autofocus>
|
<MkSelect v-if="select" v-model="selectedValue" :items="selectDef" autofocus></MkSelect>
|
||||||
<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">
|
<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="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>
|
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
||||||
|
@ -56,6 +47,8 @@ import MkModal from '@/components/MkModal.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import 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';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
type Input = {
|
type Input = {
|
||||||
|
@ -67,17 +60,9 @@ type Input = {
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SelectItem = {
|
|
||||||
value: any;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Select = {
|
type Select = {
|
||||||
items: (SelectItem | {
|
items: MkSelectItem[];
|
||||||
sectionTitle: string;
|
default: OptionValue | null;
|
||||||
items: SelectItem[];
|
|
||||||
})[];
|
|
||||||
default: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Result = string | number | true | null;
|
type Result = string | number | true | null;
|
||||||
|
@ -115,7 +100,6 @@ const emit = defineEmits<{
|
||||||
const modal = useTemplateRef('modal');
|
const modal = useTemplateRef('modal');
|
||||||
|
|
||||||
const inputValue = ref<string | number | null>(props.input?.default ?? null);
|
const inputValue = ref<string | number | null>(props.input?.default ?? null);
|
||||||
const selectedValue = ref(props.select?.default ?? null);
|
|
||||||
|
|
||||||
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
|
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
|
||||||
if (props.input) {
|
if (props.input) {
|
||||||
|
@ -134,6 +118,14 @@ const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'character
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
def: selectDef,
|
||||||
|
model: selectedValue,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: computed(() => props.select?.items ?? []),
|
||||||
|
initialValue: props.select?.default ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
// overload function を使いたいので lint エラーを無視する
|
// overload function を使いたいので lint エラーを無視する
|
||||||
function done(canceled: true): void;
|
function done(canceled: true): void;
|
||||||
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
|
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
|
||||||
|
|
|
@ -52,11 +52,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #suffix>px</template>
|
<template #suffix>px</template>
|
||||||
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
|
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-model="colorMode">
|
<MkSelect v-model="colorMode" :items="colorModeDef">
|
||||||
<template #label>{{ i18n.ts.theme }}</template>
|
<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>
|
</MkSelect>
|
||||||
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
|
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
|
||||||
<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
|
<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
|
||||||
|
@ -105,6 +102,7 @@ import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||||
import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js';
|
import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js';
|
||||||
|
|
||||||
|
@ -162,7 +160,18 @@ const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(pro
|
||||||
const header = ref(props.params?.header ?? true);
|
const header = ref(props.params?.header ?? true);
|
||||||
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? null : 500);
|
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? null : 500);
|
||||||
|
|
||||||
const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
|
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 rounded = ref(props.params?.rounded ?? true);
|
const rounded = ref(props.params?.rounded ?? true);
|
||||||
const border = ref(props.params?.border ?? true);
|
const border = ref(props.params?.border ?? true);
|
||||||
|
|
||||||
|
|
|
@ -530,6 +530,14 @@ defineExpose({
|
||||||
--eachSize: 50px;
|
--eachSize: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.s4 {
|
||||||
|
--eachSize: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.s5 {
|
||||||
|
--eachSize: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
&.w1 {
|
&.w1 {
|
||||||
width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
|
width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
|
||||||
--columns: 1fr 1fr 1fr 1fr 1fr;
|
--columns: 1fr 1fr 1fr 1fr 1fr;
|
||||||
|
|
|
@ -39,9 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-text="v.label || k"></span>
|
<span v-text="v.label || k"></span>
|
||||||
<template v-if="v.description" #caption>{{ v.description }}</template>
|
<template v-if="v.description" #caption>{{ v.description }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]">
|
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)">
|
||||||
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
<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>
|
</MkSelect>
|
||||||
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
|
<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>
|
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
|
@ -77,7 +76,8 @@ import MkRange from './MkRange.vue';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
import MkRadios from './MkRadios.vue';
|
import MkRadios from './MkRadios.vue';
|
||||||
import XFile from './MkFormDialog.file.vue';
|
import XFile from './MkFormDialog.file.vue';
|
||||||
import type { EnumItem, Form, RadioFormItem } from '@/utility/form.js';
|
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||||
|
import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
@ -120,16 +120,14 @@ function cancel() {
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEnumLabel(e: EnumItem) {
|
function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
|
||||||
return typeof e === 'string' ? e : e.label;
|
return def.enum.map((v) => {
|
||||||
}
|
if (typeof v === 'string') {
|
||||||
|
return { value: v, label: v };
|
||||||
function getEnumValue(e: EnumItem) {
|
} else {
|
||||||
return typeof e === 'string' ? e : e.value;
|
return { value: v.value, label: v.label };
|
||||||
}
|
}
|
||||||
|
});
|
||||||
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]) {
|
function getRadioKey(e: RadioFormItem['options'][number]) {
|
||||||
|
|
|
@ -19,9 +19,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div :class="$style.container">
|
<div :class="$style.container">
|
||||||
<div :class="$style.preview">
|
<div :class="$style.preview">
|
||||||
<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
|
<canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown="onImagePointerdown"></canvas>
|
||||||
<div :class="$style.previewContainer">
|
<div :class="$style.previewContainer">
|
||||||
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
|
<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">
|
<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 = false">Before</button>
|
||||||
<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
|
<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
|
||||||
|
@ -212,6 +215,147 @@ watch(enabled, () => {
|
||||||
renderer.render();
|
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>
|
</script>
|
||||||
|
|
||||||
<style module>
|
<style module>
|
||||||
|
@ -251,6 +395,18 @@ watch(enabled, () => {
|
||||||
font-size: 85%;
|
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 {
|
.previewControls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
@ -283,9 +439,13 @@ watch(enabled, () => {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
/* なんかiOSでレンダリングがおかしい
|
||||||
height: 100%;
|
width: stretch;
|
||||||
padding: 20px;
|
height: stretch;
|
||||||
|
*/
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
margin: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,31 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #header>Chart</template>
|
<template #header>Chart</template>
|
||||||
<div :class="$style.chart">
|
<div :class="$style.chart">
|
||||||
<div class="selects">
|
<div class="selects">
|
||||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
<MkSelect v-model="chartSrc" :items="chartSrcDef" style="margin: 0; flex: 1;"></MkSelect>
|
||||||
<optgroup v-if="shouldShowFederation" :label="i18n.ts.federation">
|
<MkSelect v-model="chartSpan" :items="chartSpanDef" style="margin: 0 0 0 10px;"></MkSelect>
|
||||||
<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>
|
||||||
<div class="chart _panel">
|
<div class="chart _panel">
|
||||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
|
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
|
||||||
|
@ -43,13 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<MkFoldableSection class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Active users heatmap</template>
|
<template #header>Active users heatmap</template>
|
||||||
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
|
<MkSelect v-model="heatmapSrc" :items="heatmapSrcDef" style="margin: 0 0 12px 0;"></MkSelect>
|
||||||
<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">
|
<div class="_panel" :class="$style.heatmap">
|
||||||
<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
|
<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,10 +55,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref, computed, useTemplateRef } from 'vue';
|
import { onMounted, computed, useTemplateRef } from 'vue';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
import type { HeatmapSource } from '@/components/MkHeatmap.vue';
|
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
import type { MkSelectItem, ItemOption } from '@/components/MkSelect.vue';
|
||||||
import MkChart from '@/components/MkChart.vue';
|
import MkChart from '@/components/MkChart.vue';
|
||||||
import type { ChartSrc } from '@/components/MkChart.vue';
|
import type { ChartSrc } from '@/components/MkChart.vue';
|
||||||
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
||||||
|
@ -101,15 +72,96 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
||||||
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
|
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
|
||||||
import { initChart } from '@/utility/init-chart.js';
|
import { initChart } from '@/utility/init-chart.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
initChart();
|
initChart();
|
||||||
|
|
||||||
const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator);
|
const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator);
|
||||||
|
|
||||||
const chartLimit = 500;
|
const chartLimit = 500;
|
||||||
const chartSpan = ref<'hour' | 'day'>('hour');
|
const {
|
||||||
const chartSrc = ref<ChartSrc>('active-users');
|
model: chartSpan,
|
||||||
const heatmapSrc = ref<HeatmapSource>('active-users');
|
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 subDoughnutEl = useTemplateRef('subDoughnutEl');
|
const subDoughnutEl = useTemplateRef('subDoughnutEl');
|
||||||
const pubDoughnutEl = useTemplateRef('pubDoughnutEl');
|
const pubDoughnutEl = useTemplateRef('pubDoughnutEl');
|
||||||
|
|
||||||
|
|
|
@ -15,101 +15,151 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
@esc="cancel()"
|
@esc="cancel()"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
{{ i18n.ts.drafts }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
|
{{ i18n.ts.draftsAndScheduledNotes }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
|
||||||
</template>
|
</template>
|
||||||
<div class="_spacer">
|
|
||||||
<MkPagination :paginator="paginator" withControl>
|
|
||||||
<template #empty>
|
|
||||||
<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #default="{ items }">
|
<MkStickyContainer>
|
||||||
<div class="_gaps_s">
|
<template #header>
|
||||||
<div
|
<MkTabs
|
||||||
v-for="draft in (items as unknown as Misskey.entities.NoteDraft[])"
|
v-model:tab="tab"
|
||||||
:key="draft.id"
|
centered
|
||||||
v-panel
|
:class="$style.tabs"
|
||||||
:class="[$style.draft]"
|
:tabs="[
|
||||||
>
|
{
|
||||||
<div :class="$style.draftBody" class="_gaps_s">
|
key: 'drafts',
|
||||||
<div :class="$style.draftInfo">
|
title: i18n.ts.drafts,
|
||||||
<div :class="$style.draftMeta">
|
icon: 'ti ti-pencil-question',
|
||||||
<div v-if="draft.reply" class="_nowrap">
|
},
|
||||||
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
|
{
|
||||||
<template #user>
|
key: 'scheduled',
|
||||||
<Mfm v-if="draft.reply.user.name != null" :text="draft.reply.user.name" :plain="true" :nowrap="true"/>
|
title: i18n.ts.scheduled,
|
||||||
<MkAcct v-else :user="draft.reply.user"/>
|
icon: 'ti ti-calendar-clock',
|
||||||
</template>
|
},
|
||||||
</I18n>
|
]"
|
||||||
</div>
|
/>
|
||||||
<div v-else-if="draft.replyId" class="_nowrap">
|
</template>
|
||||||
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
|
|
||||||
<template #user>
|
<div class="_spacer">
|
||||||
{{ i18n.ts.deletedNote }}
|
<MkPagination :key="tab" :paginator="tab === 'scheduled' ? scheduledPaginator : draftsPaginator" withControl>
|
||||||
</template>
|
<template #empty>
|
||||||
</I18n>
|
<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
|
||||||
</div>
|
</template>
|
||||||
<div v-if="draft.renote && draft.text != null" class="_nowrap">
|
|
||||||
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
|
<template #default="{ items }">
|
||||||
<template #user>
|
<div class="_gaps_s">
|
||||||
<Mfm v-if="draft.renote.user.name != null" :text="draft.renote.user.name" :plain="true" :nowrap="true"/>
|
<div
|
||||||
<MkAcct v-else :user="draft.renote.user"/>
|
v-for="draft in (items as unknown as Misskey.entities.NoteDraft[])"
|
||||||
</template>
|
:key="draft.id"
|
||||||
</I18n>
|
v-panel
|
||||||
</div>
|
:class="[$style.draft]"
|
||||||
<div v-else-if="draft.renoteId" class="_nowrap">
|
>
|
||||||
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
|
<div :class="$style.draftBody" class="_gaps_s">
|
||||||
<template #user>
|
<MkInfo v-if="draft.scheduledAt != null && draft.isActuallyScheduled">
|
||||||
{{ i18n.ts.deletedNote }}
|
<I18n :src="i18n.ts.scheduledToPostOnX" tag="span">
|
||||||
</template>
|
<template #x>
|
||||||
</I18n>
|
<MkTime :time="draft.scheduledAt" :mode="'detail'" style="font-weight: bold;"/>
|
||||||
</div>
|
</template>
|
||||||
<div v-if="draft.channel" class="_nowrap">
|
</I18n>
|
||||||
<i class="ti ti-device-tv"></i> {{ i18n.tsx._drafts.postTo({ channel: draft.channel.name }) }}
|
</MkInfo>
|
||||||
|
<div :class="$style.draftInfo">
|
||||||
|
<div :class="$style.draftMeta">
|
||||||
|
<div v-if="draft.reply" class="_nowrap">
|
||||||
|
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
|
||||||
|
<template #user>
|
||||||
|
<Mfm v-if="draft.reply.user.name != null" :text="draft.reply.user.name" :plain="true" :nowrap="true"/>
|
||||||
|
<MkAcct v-else :user="draft.reply.user"/>
|
||||||
|
</template>
|
||||||
|
</I18n>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="draft.replyId" class="_nowrap">
|
||||||
|
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
|
||||||
|
<template #user>
|
||||||
|
{{ i18n.ts.deletedNote }}
|
||||||
|
</template>
|
||||||
|
</I18n>
|
||||||
|
</div>
|
||||||
|
<div v-if="draft.renote && draft.text != null" class="_nowrap">
|
||||||
|
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
|
||||||
|
<template #user>
|
||||||
|
<Mfm v-if="draft.renote.user.name != null" :text="draft.renote.user.name" :plain="true" :nowrap="true"/>
|
||||||
|
<MkAcct v-else :user="draft.renote.user"/>
|
||||||
|
</template>
|
||||||
|
</I18n>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="draft.renoteId" class="_nowrap">
|
||||||
|
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
|
||||||
|
<template #user>
|
||||||
|
{{ i18n.ts.deletedNote }}
|
||||||
|
</template>
|
||||||
|
</I18n>
|
||||||
|
</div>
|
||||||
|
<div v-if="draft.channel" class="_nowrap">
|
||||||
|
<i class="ti ti-device-tv"></i> {{ i18n.tsx._drafts.postTo({ channel: draft.channel.name }) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div :class="$style.draftContent">
|
||||||
<div :class="$style.draftContent">
|
<Mfm :text="getNoteSummary(draft, { showRenote: false, showReply: false })" :plain="true" :author="draft.user"/>
|
||||||
<Mfm :text="getNoteSummary(draft, { showRenote: false, showReply: false })" :plain="true" :author="draft.user"/>
|
</div>
|
||||||
</div>
|
<div :class="$style.draftFooter">
|
||||||
<div :class="$style.draftFooter">
|
<div :class="$style.draftVisibility">
|
||||||
<div :class="$style.draftVisibility">
|
<span :title="i18n.ts._visibility[draft.visibility]">
|
||||||
<span :title="i18n.ts._visibility[draft.visibility]">
|
<i v-if="draft.visibility === 'public'" class="ti ti-world"></i>
|
||||||
<i v-if="draft.visibility === 'public'" class="ti ti-world"></i>
|
<i v-else-if="draft.visibility === 'home'" class="ti ti-home"></i>
|
||||||
<i v-else-if="draft.visibility === 'home'" class="ti ti-home"></i>
|
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
|
<i v-else-if="draft.visibility === 'specified'" class="ti ti-mail"></i>
|
||||||
<i v-else-if="draft.visibility === 'specified'" class="ti ti-mail"></i>
|
</span>
|
||||||
</span>
|
<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
||||||
<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
</div>
|
||||||
|
<MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/>
|
||||||
</div>
|
</div>
|
||||||
<MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div :class="$style.draftActions" class="_buttons">
|
<div :class="$style.draftActions" class="_buttons">
|
||||||
<MkButton
|
<template v-if="draft.scheduledAt != null && draft.isActuallyScheduled">
|
||||||
:class="$style.itemButton"
|
<MkButton
|
||||||
small
|
:class="$style.itemButton"
|
||||||
@click="restoreDraft(draft)"
|
small
|
||||||
>
|
@click="cancelSchedule(draft)"
|
||||||
<i class="ti ti-corner-up-left"></i>
|
>
|
||||||
{{ i18n.ts._drafts.restore }}
|
<i class="ti ti-calendar-x"></i> {{ i18n.ts._drafts.cancelSchedule }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkButton
|
<!-- TODO
|
||||||
v-tooltip="i18n.ts._drafts.delete"
|
<MkButton
|
||||||
danger
|
:class="$style.itemButton"
|
||||||
small
|
small
|
||||||
:iconOnly="true"
|
@click="reSchedule(draft)"
|
||||||
:class="$style.itemButton"
|
>
|
||||||
@click="deleteDraft(draft)"
|
<i class="ti ti-calendar-time"></i> {{ i18n.ts._drafts.reSchedule }}
|
||||||
>
|
</MkButton>
|
||||||
<i class="ti ti-trash"></i>
|
-->
|
||||||
</MkButton>
|
</template>
|
||||||
|
<MkButton
|
||||||
|
v-else
|
||||||
|
:class="$style.itemButton"
|
||||||
|
small
|
||||||
|
@click="restoreDraft(draft)"
|
||||||
|
>
|
||||||
|
<i class="ti ti-corner-up-left"></i> {{ i18n.ts._drafts.restore }}
|
||||||
|
</MkButton>
|
||||||
|
<MkButton
|
||||||
|
v-tooltip="i18n.ts._drafts.delete"
|
||||||
|
danger
|
||||||
|
small
|
||||||
|
:iconOnly="true"
|
||||||
|
:class="$style.itemButton"
|
||||||
|
style="margin-left: auto;"
|
||||||
|
@click="deleteDraft(draft)"
|
||||||
|
>
|
||||||
|
<i class="ti ti-trash"></i>
|
||||||
|
</MkButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</MkPagination>
|
||||||
</MkPagination>
|
</div>
|
||||||
</div>
|
</MkStickyContainer>
|
||||||
</MkModalWindow>
|
</MkModalWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -125,6 +175,12 @@ import * as os from '@/os.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api';
|
import { misskeyApi } from '@/utility/misskey-api';
|
||||||
import { Paginator } from '@/utility/paginator.js';
|
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<{
|
const emit = defineEmits<{
|
||||||
(ev: 'restore', draft: Misskey.entities.NoteDraft): void;
|
(ev: 'restore', draft: Misskey.entities.NoteDraft): void;
|
||||||
|
@ -132,8 +188,20 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const paginator = markRaw(new Paginator('notes/drafts/list', {
|
const tab = ref<'drafts' | 'scheduled'>(props.scheduled ? 'scheduled' : 'drafts');
|
||||||
|
|
||||||
|
const draftsPaginator = markRaw(new Paginator('notes/drafts/list', {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
params: {
|
||||||
|
scheduled: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const scheduledPaginator = markRaw(new Paginator('notes/drafts/list', {
|
||||||
|
limit: 10,
|
||||||
|
params: {
|
||||||
|
scheduled: true,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const currentDraftsCount = ref(0);
|
const currentDraftsCount = ref(0);
|
||||||
|
@ -162,7 +230,17 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => {
|
os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => {
|
||||||
paginator.reload();
|
draftsPaginator.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelSchedule(draft: Misskey.entities.NoteDraft) {
|
||||||
|
os.apiWithDialog('notes/drafts/update', {
|
||||||
|
draftId: draft.id,
|
||||||
|
isActuallyScheduled: false,
|
||||||
|
scheduledAt: null,
|
||||||
|
}).then(() => {
|
||||||
|
scheduledPaginator.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -220,4 +298,11 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
border-top: solid 1px var(--MI_THEME-divider);
|
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>
|
</style>
|
||||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div :class="$style.head">
|
<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-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'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'createToken', 'scheduledNotePosted', 'scheduledNotePostFailed'].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' && 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 === '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>
|
<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,6 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
[$style.t_mention]: notification.type === 'mention',
|
[$style.t_mention]: notification.type === 'mention',
|
||||||
[$style.t_quote]: notification.type === 'quote',
|
[$style.t_quote]: notification.type === 'quote',
|
||||||
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
[$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_achievementEarned]: notification.type === 'achievementEarned',
|
||||||
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
|
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
|
||||||
[$style.t_login]: notification.type === 'login',
|
[$style.t_login]: notification.type === 'login',
|
||||||
|
@ -39,6 +41,8 @@ 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 === 'mention'" class="ti ti-at"></i>
|
||||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></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 === '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 === 'achievementEarned'" class="ti ti-medal"></i>
|
||||||
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></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>
|
<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
|
||||||
|
@ -60,6 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.tail">
|
<div :class="$style.tail">
|
||||||
<header :class="$style.header">
|
<header :class="$style.header">
|
||||||
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
|
<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 === '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 === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
|
||||||
<span v-else-if="notification.type === 'chatRoomInvitationReceived'">{{ i18n.ts._notification.chatRoomInvitationReceived }}</span>
|
<span v-else-if="notification.type === 'chatRoomInvitationReceived'">{{ i18n.ts._notification.chatRoomInvitationReceived }}</span>
|
||||||
|
@ -103,6 +109,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
</MkA>
|
</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">
|
<div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
|
||||||
{{ notification.role.name }}
|
{{ notification.role.name }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -338,6 +349,16 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.t_scheduledNotePosted {
|
||||||
|
background: var(--eventOther);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t_scheduledNotePostFailed {
|
||||||
|
background: var(--eventOther);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.t_achievementEarned {
|
.t_achievementEarned {
|
||||||
background: var(--eventAchievement);
|
background: var(--eventAchievement);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div :class="$style.control">
|
<div :class="$style.control">
|
||||||
<MkSelect v-model="order" :class="$style.order" :items="[{ label: i18n.ts._order.newest, value: 'newest' }, { label: i18n.ts._order.oldest, value: 'oldest' }]">
|
<MkSelect v-model="order" :class="$style.order" :items="orderDef">
|
||||||
<template #prefix><i class="ti ti-arrows-sort"></i></template>
|
<template #prefix><i class="ti ti-arrows-sort"></i></template>
|
||||||
</MkSelect>
|
</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>
|
<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,6 +45,7 @@ import { i18n } from '@/i18n.js';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
paginator: T;
|
paginator: T;
|
||||||
|
@ -58,7 +59,16 @@ const props = withDefaults(defineProps<{
|
||||||
const searchOpened = ref(false);
|
const searchOpened = ref(false);
|
||||||
const filterOpened = ref(props.filterOpened);
|
const filterOpened = ref(props.filterOpened);
|
||||||
|
|
||||||
const order = ref<'newest' | 'oldest'>('newest');
|
const {
|
||||||
|
model: order,
|
||||||
|
def: orderDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts._order.newest, value: 'newest' },
|
||||||
|
{ label: i18n.ts._order.oldest, value: 'oldest' },
|
||||||
|
],
|
||||||
|
initialValue: 'newest',
|
||||||
|
});
|
||||||
const date = ref<number | null>(null);
|
const date = ref<number | null>(null);
|
||||||
const q = ref<string | null>(null);
|
const q = ref<string | null>(null);
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.root, accented ? $style.accented : null]"></div>
|
<div :class="[$style.root, accented ? $style.accented : null, revered ? $style.revered : null]"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
accented?: boolean;
|
accented?: boolean;
|
||||||
|
revered?: boolean;
|
||||||
|
height?: number;
|
||||||
}>(), {
|
}>(), {
|
||||||
accented: false,
|
accented: false,
|
||||||
|
revered: false,
|
||||||
|
height: 200,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -27,14 +31,17 @@ const props = withDefaults(defineProps<{
|
||||||
--dot-size: 2px;
|
--dot-size: 2px;
|
||||||
--gap-size: 40px;
|
--gap-size: 40px;
|
||||||
--offset: calc(var(--gap-size) / 2);
|
--offset: calc(var(--gap-size) / 2);
|
||||||
|
--height: v-bind('props.height + "px"');
|
||||||
|
|
||||||
height: 200px;
|
height: var(--height);
|
||||||
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-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-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);
|
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%);
|
mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.revered {
|
||||||
|
mask-image: linear-gradient(to top, black 0%, transparent 100%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { host } from '@@/js/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
|
||||||
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
||||||
import { sum } from '@/utility/array.js';
|
import { sum } from '@/utility/array.js';
|
||||||
import { pleaseLogin } from '@/utility/please-login.js';
|
import { pleaseLogin } from '@/utility/please-login.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { useLowresTime } from '@/composables/use-lowres-time.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
noteId: string;
|
noteId: string;
|
||||||
|
@ -48,7 +48,21 @@ const props = defineProps<{
|
||||||
author?: Misskey.entities.UserLite;
|
author?: Misskey.entities.UserLite;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const remaining = ref(-1);
|
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 total = computed(() => sum(props.choices.map(x => x.votes)));
|
const total = computed(() => sum(props.choices.map(x => x.votes)));
|
||||||
const closed = computed(() => remaining.value === 0);
|
const closed = computed(() => remaining.value === 0);
|
||||||
|
@ -71,22 +85,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||||
url: `https://${host}/notes/${props.noteId}`,
|
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;
|
if (props.readOnly || closed.value || isVoted.value) return;
|
||||||
|
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
|
|
|
@ -22,11 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch>
|
<MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch>
|
||||||
<section>
|
<section>
|
||||||
<div>
|
<div>
|
||||||
<MkSelect v-model="expiration" small>
|
<MkSelect v-model="expiration" :items="expirationDef" small>
|
||||||
<template #label>{{ i18n.ts._poll.expiration }}</template>
|
<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>
|
</MkSelect>
|
||||||
<section v-if="expiration === 'at'">
|
<section v-if="expiration === 'at'">
|
||||||
<MkInput v-model="atDate" small type="date" class="input">
|
<MkInput v-model="atDate" small type="date" class="input">
|
||||||
|
@ -40,12 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInput v-model="after" small type="number" :min="1" class="input">
|
<MkInput v-model="after" small type="number" :min="1" class="input">
|
||||||
<template #label>{{ i18n.ts._poll.duration }}</template>
|
<template #label>{{ i18n.ts._poll.duration }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-model="unit" small>
|
<MkSelect v-model="unit" :items="unitDef" small></MkSelect>
|
||||||
<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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -61,6 +53,7 @@ import MkButton from './MkButton.vue';
|
||||||
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
||||||
import { addTime } from '@/utility/time.js';
|
import { addTime } from '@/utility/time.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
export type PollEditorModelValue = {
|
export type PollEditorModelValue = {
|
||||||
expiresAt: number | null;
|
expiresAt: number | null;
|
||||||
|
@ -78,11 +71,32 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const choices = ref(props.modelValue.choices);
|
const choices = ref(props.modelValue.choices);
|
||||||
const multiple = ref(props.modelValue.multiple);
|
const multiple = ref(props.modelValue.multiple);
|
||||||
const expiration = ref('infinite');
|
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 atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
|
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
|
||||||
const atTime = ref('00:00');
|
const atTime = ref('00:00');
|
||||||
const after = ref(0);
|
const after = ref(0);
|
||||||
const unit = ref('second');
|
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',
|
||||||
|
});
|
||||||
|
|
||||||
if (props.modelValue.expiresAt) {
|
if (props.modelValue.expiresAt) {
|
||||||
expiration.value = 'at';
|
expiration.value = 'at';
|
||||||
|
|
|
@ -6,15 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.root]">
|
<div :class="[$style.root]">
|
||||||
<div :class="$style.items">
|
<div :class="$style.items">
|
||||||
<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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.headerLeft">
|
<div :class="$style.headerLeft">
|
||||||
<button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button>
|
<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">
|
<button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu">
|
||||||
<MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/>
|
<img :class="$style.avatar" :src="(postAccount ?? $i).avatarUrl" style="border-radius: 100%;"/>
|
||||||
</button>
|
</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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.headerRight">
|
<div :class="$style.headerRight">
|
||||||
<template v-if="!(targetChannel != null && fixed)">
|
<template v-if="!(targetChannel != null && fixed)">
|
||||||
|
@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template v-if="posted"></template>
|
<template v-if="posted"></template>
|
||||||
<template v-else-if="posting"><MkEllipsis/></template>
|
<template v-else-if="posting"><MkEllipsis/></template>
|
||||||
<template v-else>{{ submitText }}</template>
|
<template v-else>{{ submitText }}</template>
|
||||||
<i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : replyTargetNote ? 'ti ti-arrow-back-up' : renoteTargetNote ? 'ti ti-quote' : 'ti ti-send'"></i>
|
<i style="margin-left: 6px;" :class="submitIcon"></i>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,6 +61,13 @@ 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>
|
<button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<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">
|
<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">
|
<input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd">
|
||||||
|
@ -105,7 +112,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef } from 'vue';
|
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef, onUnmounted } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||||
|
@ -199,6 +206,7 @@ if (props.initialVisibleUsers) {
|
||||||
props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
|
props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
|
||||||
}
|
}
|
||||||
const reactionAcceptance = ref(store.s.reactionAcceptance);
|
const reactionAcceptance = ref(store.s.reactionAcceptance);
|
||||||
|
const scheduledAt = ref<number | null>(null);
|
||||||
const draghover = ref(false);
|
const draghover = ref(false);
|
||||||
const quoteId = ref<string | null>(null);
|
const quoteId = ref<string | null>(null);
|
||||||
const hasNotSpecifiedMentions = ref(false);
|
const hasNotSpecifiedMentions = ref(false);
|
||||||
|
@ -218,6 +226,10 @@ const uploader = useUploader({
|
||||||
multiple: true,
|
multiple: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
uploader.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
uploader.events.on('itemUploaded', ctx => {
|
uploader.events.on('itemUploaded', ctx => {
|
||||||
files.value.push(ctx.item.uploaded!);
|
files.value.push(ctx.item.uploaded!);
|
||||||
uploader.removeItem(ctx.item);
|
uploader.removeItem(ctx.item);
|
||||||
|
@ -258,11 +270,17 @@ const placeholder = computed((): string => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitText = computed((): string => {
|
const submitText = computed((): string => {
|
||||||
return renoteTargetNote.value
|
return scheduledAt.value != null
|
||||||
? i18n.ts.quote
|
? i18n.ts.schedule
|
||||||
: replyTargetNote.value
|
: renoteTargetNote.value
|
||||||
? i18n.ts.reply
|
? i18n.ts.quote
|
||||||
: i18n.ts.note;
|
: 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 => {
|
const textLength = computed((): number => {
|
||||||
|
@ -410,6 +428,7 @@ function watchForDraft() {
|
||||||
watch(localOnly, () => saveDraft());
|
watch(localOnly, () => saveDraft());
|
||||||
watch(quoteId, () => saveDraft());
|
watch(quoteId, () => saveDraft());
|
||||||
watch(reactionAcceptance, () => saveDraft());
|
watch(reactionAcceptance, () => saveDraft());
|
||||||
|
watch(scheduledAt, () => saveDraft());
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkMissingMention() {
|
function checkMissingMention() {
|
||||||
|
@ -567,11 +586,11 @@ async function toggleReactionAcceptance() {
|
||||||
const select = await os.select({
|
const select = await os.select({
|
||||||
title: i18n.ts.reactionAcceptance,
|
title: i18n.ts.reactionAcceptance,
|
||||||
items: [
|
items: [
|
||||||
{ value: null, text: i18n.ts.all },
|
{ value: null, label: i18n.ts.all },
|
||||||
{ value: 'likeOnlyForRemote' as const, text: i18n.ts.likeOnlyForRemote },
|
{ value: 'likeOnlyForRemote' as const, label: i18n.ts.likeOnlyForRemote },
|
||||||
{ value: 'nonSensitiveOnly' as const, text: i18n.ts.nonSensitiveOnly },
|
{ value: 'nonSensitiveOnly' as const, label: i18n.ts.nonSensitiveOnly },
|
||||||
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
|
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, label: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
|
||||||
{ value: 'likeOnly' as const, text: i18n.ts.likeOnly },
|
{ value: 'likeOnly' as const, label: i18n.ts.likeOnly },
|
||||||
],
|
],
|
||||||
default: reactionAcceptance.value,
|
default: reactionAcceptance.value,
|
||||||
});
|
});
|
||||||
|
@ -601,7 +620,13 @@ function showOtherSettings() {
|
||||||
action: () => {
|
action: () => {
|
||||||
toggleReactionAcceptance();
|
toggleReactionAcceptance();
|
||||||
},
|
},
|
||||||
}, { type: 'divider' }, {
|
}, ...($i.policies.scheduledNoteLimit > 0 ? [{
|
||||||
|
icon: 'ti ti-calendar-time',
|
||||||
|
text: i18n.ts.schedulePost + '...',
|
||||||
|
action: () => {
|
||||||
|
schedule();
|
||||||
|
},
|
||||||
|
}] : []), { type: 'divider' }, {
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
icon: 'ti ti-eye',
|
icon: 'ti ti-eye',
|
||||||
text: i18n.ts.preview,
|
text: i18n.ts.preview,
|
||||||
|
@ -650,6 +675,7 @@ function clear() {
|
||||||
files.value = [];
|
files.value = [];
|
||||||
poll.value = null;
|
poll.value = null;
|
||||||
quoteId.value = null;
|
quoteId.value = null;
|
||||||
|
scheduledAt.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeydown(ev: KeyboardEvent) {
|
function onKeydown(ev: KeyboardEvent) {
|
||||||
|
@ -805,6 +831,7 @@ function saveDraft() {
|
||||||
...( visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}),
|
...( visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}),
|
||||||
quoteId: quoteId.value,
|
quoteId: quoteId.value,
|
||||||
reactionAcceptance: reactionAcceptance.value,
|
reactionAcceptance: reactionAcceptance.value,
|
||||||
|
scheduledAt: scheduledAt.value,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -819,29 +846,25 @@ function deleteDraft() {
|
||||||
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
|
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveServerDraft(clearLocal = false) {
|
async function saveServerDraft(options: {
|
||||||
|
isActuallyScheduled?: boolean;
|
||||||
|
} = {}) {
|
||||||
return await os.apiWithDialog(serverDraftId.value == null ? 'notes/drafts/create' : 'notes/drafts/update', {
|
return await os.apiWithDialog(serverDraftId.value == null ? 'notes/drafts/create' : 'notes/drafts/update', {
|
||||||
...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }),
|
...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }),
|
||||||
text: text.value,
|
text: text.value,
|
||||||
useCw: useCw.value,
|
cw: useCw.value ? cw.value || null : null,
|
||||||
cw: cw.value,
|
|
||||||
visibility: visibility.value,
|
visibility: visibility.value,
|
||||||
localOnly: localOnly.value,
|
localOnly: localOnly.value,
|
||||||
hashtag: hashtags.value,
|
hashtag: hashtags.value,
|
||||||
...(files.value.length > 0 ? { fileIds: files.value.map(f => f.id) } : {}),
|
fileIds: files.value.map(f => f.id),
|
||||||
poll: poll.value,
|
poll: poll.value,
|
||||||
...(visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}),
|
visibleUserIds: visibleUsers.value.map(x => x.id),
|
||||||
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : undefined,
|
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : quoteId.value ? quoteId.value : null,
|
||||||
replyId: replyTargetNote.value ? replyTargetNote.value.id : undefined,
|
replyId: replyTargetNote.value ? replyTargetNote.value.id : null,
|
||||||
quoteId: quoteId.value,
|
channelId: targetChannel.value ? targetChannel.value.id : null,
|
||||||
channelId: targetChannel.value ? targetChannel.value.id : undefined,
|
|
||||||
reactionAcceptance: reactionAcceptance.value,
|
reactionAcceptance: reactionAcceptance.value,
|
||||||
}).then(() => {
|
scheduledAt: scheduledAt.value,
|
||||||
if (clearLocal) {
|
isActuallyScheduled: options.isActuallyScheduled ?? false,
|
||||||
clear();
|
|
||||||
deleteDraft();
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -876,6 +899,21 @@ 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 (props.mock) return;
|
||||||
|
|
||||||
if (visibility.value === 'public' && (
|
if (visibility.value === 'public' && (
|
||||||
|
@ -1047,6 +1085,14 @@ async function post(ev?: MouseEvent) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function postAsScheduled() {
|
||||||
|
if (props.mock) return;
|
||||||
|
|
||||||
|
await saveServerDraft({
|
||||||
|
isActuallyScheduled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
emit('cancel');
|
emit('cancel');
|
||||||
}
|
}
|
||||||
|
@ -1141,8 +1187,10 @@ function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDraftMenu(ev: MouseEvent) {
|
function showDraftMenu(ev: MouseEvent) {
|
||||||
function showDraftsDialog() {
|
function showDraftsDialog(scheduled: boolean) {
|
||||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNoteDraftsDialog.vue')), {}, {
|
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNoteDraftsDialog.vue')), {
|
||||||
|
scheduled,
|
||||||
|
}, {
|
||||||
restore: async (draft: Misskey.entities.NoteDraft) => {
|
restore: async (draft: Misskey.entities.NoteDraft) => {
|
||||||
text.value = draft.text ?? '';
|
text.value = draft.text ?? '';
|
||||||
useCw.value = draft.cw != null;
|
useCw.value = draft.cw != null;
|
||||||
|
@ -1173,6 +1221,7 @@ function showDraftMenu(ev: MouseEvent) {
|
||||||
renoteTargetNote.value = draft.renote;
|
renoteTargetNote.value = draft.renote;
|
||||||
replyTargetNote.value = draft.reply;
|
replyTargetNote.value = draft.reply;
|
||||||
reactionAcceptance.value = draft.reactionAcceptance;
|
reactionAcceptance.value = draft.reactionAcceptance;
|
||||||
|
scheduledAt.value = draft.scheduledAt ?? null;
|
||||||
if (draft.channel) targetChannel.value = draft.channel as unknown as Misskey.entities.Channel;
|
if (draft.channel) targetChannel.value = draft.channel as unknown as Misskey.entities.Channel;
|
||||||
|
|
||||||
visibleUsers.value = [];
|
visibleUsers.value = [];
|
||||||
|
@ -1213,11 +1262,32 @@ function showDraftMenu(ev: MouseEvent) {
|
||||||
text: i18n.ts._drafts.listDrafts,
|
text: i18n.ts._drafts.listDrafts,
|
||||||
icon: 'ti ti-cloud-download',
|
icon: 'ti ti-cloud-download',
|
||||||
action: () => {
|
action: () => {
|
||||||
showDraftsDialog();
|
showDraftsDialog(false);
|
||||||
|
},
|
||||||
|
}, { type: 'divider' }, {
|
||||||
|
type: 'button',
|
||||||
|
text: i18n.ts._drafts.listScheduledNotes,
|
||||||
|
icon: 'ti ti-clock-down',
|
||||||
|
action: () => {
|
||||||
|
showDraftsDialog(true);
|
||||||
},
|
},
|
||||||
}], (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
}], (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(() => {
|
onMounted(() => {
|
||||||
if (props.autofocus) {
|
if (props.autofocus) {
|
||||||
focus();
|
focus();
|
||||||
|
@ -1253,6 +1323,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
quoteId.value = draft.data.quoteId;
|
quoteId.value = draft.data.quoteId;
|
||||||
reactionAcceptance.value = draft.data.reactionAcceptance;
|
reactionAcceptance.value = draft.data.reactionAcceptance;
|
||||||
|
scheduledAt.value = draft.data.scheduledAt ?? null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1302,6 +1373,7 @@ async function canClose() {
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
clear,
|
clear,
|
||||||
|
abortUploader: () => uploader.abortAll(),
|
||||||
canClose,
|
canClose,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1516,6 +1588,10 @@ html[data-color-scheme=light] .preview {
|
||||||
margin: 0 20px 16px 20px;
|
margin: 0 20px 16px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scheduledAt {
|
||||||
|
margin: 0 20px 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.cw,
|
.cw,
|
||||||
.hashtags,
|
.hashtags,
|
||||||
.text {
|
.text {
|
||||||
|
|
|
@ -54,6 +54,7 @@ function onPosted() {
|
||||||
async function _close() {
|
async function _close() {
|
||||||
const canClose = await form.value?.canClose();
|
const canClose = await form.value?.canClose();
|
||||||
if (!canClose) return;
|
if (!canClose) return;
|
||||||
|
form.value?.abortUploader();
|
||||||
modal.value?.close();
|
modal.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ function subscribe() {
|
||||||
publickey: encode(subscription.getKey('p256dh')),
|
publickey: encode(subscription.getKey('p256dh')),
|
||||||
});
|
});
|
||||||
}, async err => { // When subscribe failed
|
}, async err => { // When subscribe failed
|
||||||
// 通知が許可されていなかったとき
|
// 通知が許可されていなかったとき
|
||||||
if (err?.name === 'NotAllowedError') {
|
if (err?.name === 'NotAllowedError') {
|
||||||
console.info('User denied the notification permission request.');
|
console.info('User denied the notification permission request.');
|
||||||
return;
|
return;
|
||||||
|
@ -114,14 +114,13 @@ async function unsubscribe() {
|
||||||
|
|
||||||
if ($i && accounts.length >= 2) {
|
if ($i && accounts.length >= 2) {
|
||||||
apiWithDialog('sw/unregister', {
|
apiWithDialog('sw/unregister', {
|
||||||
i: $i.token,
|
|
||||||
endpoint,
|
endpoint,
|
||||||
});
|
}, $i.token);
|
||||||
} else {
|
} else {
|
||||||
pushSubscription.value.unsubscribe();
|
pushSubscription.value.unsubscribe();
|
||||||
apiWithDialog('sw/unregister', {
|
apiWithDialog('sw/unregister', {
|
||||||
endpoint,
|
endpoint,
|
||||||
});
|
}, null);
|
||||||
pushSubscription.value = null;
|
pushSubscription.value = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,7 +133,7 @@ function encode(buffer: ArrayBuffer | null) {
|
||||||
* Convert the URL safe base64 string to a Uint8Array
|
* Convert the URL safe base64 string to a Uint8Array
|
||||||
* @param base64String base64 string
|
* @param base64String base64 string
|
||||||
*/
|
*/
|
||||||
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
function urlBase64ToUint8Array(base64String: string): BufferSource {
|
||||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||||
const base64 = (base64String + padding)
|
const base64 = (base64String + padding)
|
||||||
.replace(/-/g, '+')
|
.replace(/-/g, '+')
|
||||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
|
<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
|
||||||
<template v-if="forModeration">
|
<template v-if="forModeration">
|
||||||
<i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i>
|
<i v-if="'isPublic' in role && 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>
|
<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--MI_THEME-warn)"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
<span :class="$style.bodyName">{{ role.name }}</span>
|
<span :class="$style.bodyName">{{ role.name }}</span>
|
||||||
<template v-if="detailed">
|
<template v-if="detailed && 'target' in role && 'usersCount' in role">
|
||||||
<span v-if="role.target === 'manual'" :class="$style.bodyUsers">{{ role.usersCount }} users</span>
|
<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>
|
<span v-else-if="role.target === 'conditional'" :class="$style.bodyUsers">? users</span>
|
||||||
</template>
|
</template>
|
||||||
|
@ -39,7 +39,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
role: Misskey.entities.Role;
|
role: Misskey.entities.Role | Misskey.entities.IResponse['roles'][number];
|
||||||
forModeration: boolean;
|
forModeration: boolean;
|
||||||
detailed?: boolean;
|
detailed?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
|
|
|
@ -102,12 +102,12 @@ async function addRole() {
|
||||||
const items = roles.value
|
const items = roles.value
|
||||||
.filter(r => r.isPublic)
|
.filter(r => r.isPublic)
|
||||||
.filter(r => !selectedRoleIds.value.includes(r.id))
|
.filter(r => !selectedRoleIds.value.includes(r.id))
|
||||||
.map(r => ({ text: r.name, value: r }));
|
.map(r => ({ label: r.name, value: r.id }));
|
||||||
|
|
||||||
const { canceled, result: role } = await os.select({ items });
|
const { canceled, result: roleId } = await os.select({ items });
|
||||||
if (canceled || role == null) return;
|
if (canceled || roleId == null) return;
|
||||||
|
|
||||||
selectedRoleIds.value.push(role.id);
|
selectedRoleIds.value.push(roleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeRole(roleId: string) {
|
async function removeRole(roleId: string) {
|
||||||
|
|
|
@ -40,46 +40,41 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
type ItemOption = {
|
export type OptionValue = string | number | null;
|
||||||
|
|
||||||
|
export type ItemOption<T extends OptionValue = OptionValue> = {
|
||||||
type?: 'option';
|
type?: 'option';
|
||||||
value: string | number | null;
|
value: T;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ItemGroup = {
|
export type ItemGroup<T extends OptionValue = OptionValue> = {
|
||||||
type: 'group';
|
type: 'group';
|
||||||
label: string;
|
label?: string;
|
||||||
items: ItemOption[];
|
items: ItemOption<T>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MkSelectItem = ItemOption | ItemGroup;
|
export type MkSelectItem<T extends OptionValue = OptionValue> = ItemOption<T> | ItemGroup<T>;
|
||||||
|
|
||||||
type ValuesOfItems<T> = T extends (infer U)[]
|
export type GetMkSelectValueType<T extends MkSelectItem> = T extends ItemGroup
|
||||||
? U extends { type: 'group'; items: infer V }
|
? T['items'][number]['value']
|
||||||
? V extends (infer W)[]
|
: T extends ItemOption
|
||||||
? W extends { value: infer X }
|
? T['value']
|
||||||
? X
|
: never;
|
||||||
: never
|
|
||||||
: never
|
export type GetMkSelectValueTypesFromDef<T extends MkSelectItem[]> = T[number] extends MkSelectItem
|
||||||
: U extends { value: infer Y }
|
? GetMkSelectValueType<T[number]>
|
||||||
? Y
|
|
||||||
: never
|
|
||||||
: never;
|
: never;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup generic="T extends MkSelectItem[]">
|
<script lang="ts" setup generic="const ITEMS extends MkSelectItem[], MODELT extends OptionValue">
|
||||||
import { onMounted, nextTick, ref, watch, computed, toRefs, useSlots } from 'vue';
|
import { onMounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
import type { VNode, VNodeChild } from 'vue';
|
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
// TODO: itemsをslot内のoptionで指定する用法は廃止する(props.itemsを必須化する)
|
|
||||||
// see: https://github.com/misskey-dev/misskey/issues/15558
|
|
||||||
// あと型推論と相性が良くない
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: ValuesOfItems<T>;
|
items: ITEMS;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
@ -88,16 +83,17 @@ const props = defineProps<{
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
small?: boolean;
|
small?: boolean;
|
||||||
large?: boolean;
|
large?: boolean;
|
||||||
items?: T;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
type ModelTChecked = MODELT & (
|
||||||
(ev: 'update:modelValue', value: ValuesOfItems<T>): void;
|
MODELT extends GetMkSelectValueTypesFromDef<ITEMS>
|
||||||
}>();
|
? unknown
|
||||||
|
: 'Error: The type of model does not match the type of items.'
|
||||||
|
);
|
||||||
|
|
||||||
const slots = useSlots();
|
const model = defineModel<ModelTChecked>({ required: true });
|
||||||
|
|
||||||
const { modelValue, autofocus } = toRefs(props);
|
const { autofocus } = toRefs(props);
|
||||||
const focused = ref(false);
|
const focused = ref(false);
|
||||||
const opening = ref(false);
|
const opening = ref(false);
|
||||||
const currentValueText = ref<string | null>(null);
|
const currentValueText = ref<string | null>(null);
|
||||||
|
@ -140,52 +136,26 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
watch([modelValue, () => props.items], () => {
|
watch([model, () => props.items], () => {
|
||||||
if (props.items) {
|
let found: ItemOption | null = null;
|
||||||
let found: ItemOption | null = null;
|
for (const item of props.items) {
|
||||||
for (const item of props.items) {
|
if (item.type === 'group') {
|
||||||
if (item.type === 'group') {
|
for (const option of item.items) {
|
||||||
for (const option of item.items) {
|
if (option.value === model.value) {
|
||||||
if (option.value === modelValue.value) {
|
found = option;
|
||||||
found = option;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (item.value === modelValue.value) {
|
|
||||||
found = item;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (item.value === model.value) {
|
||||||
|
found = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (found) {
|
|
||||||
currentValueText.value = found.label;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
if (found) {
|
||||||
const scanOptions = (options: VNodeChild[]) => {
|
currentValueText.value = found.label;
|
||||||
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 });
|
}, { immediate: true, deep: true });
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
|
@ -196,68 +166,32 @@ function show() {
|
||||||
|
|
||||||
const menu: MenuItem[] = [];
|
const menu: MenuItem[] = [];
|
||||||
|
|
||||||
if (props.items) {
|
for (const item of props.items) {
|
||||||
for (const item of props.items) {
|
if (item.type === 'group') {
|
||||||
if (item.type === 'group') {
|
if (item.label != null) {
|
||||||
menu.push({
|
menu.push({
|
||||||
type: 'label',
|
type: 'label',
|
||||||
text: item.label,
|
text: item.label,
|
||||||
});
|
});
|
||||||
for (const option of item.items) {
|
}
|
||||||
menu.push({
|
for (const option of item.items) {
|
||||||
text: option.label,
|
|
||||||
active: computed(() => modelValue.value === option.value),
|
|
||||||
action: () => {
|
|
||||||
emit('update:modelValue', option.value);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
menu.push({
|
menu.push({
|
||||||
text: item.label,
|
text: option.label,
|
||||||
active: computed(() => modelValue.value === item.value),
|
active: computed(() => model.value === option.value),
|
||||||
action: () => {
|
action: () => {
|
||||||
emit('update:modelValue', item.value);
|
model.value = option.value as ModelTChecked;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
|
||||||
let options = slots.default!();
|
|
||||||
|
|
||||||
const pushOption = (option: VNode) => {
|
|
||||||
menu.push({
|
menu.push({
|
||||||
text: option.children as string,
|
text: item.label,
|
||||||
active: computed(() => modelValue.value === option.props?.value),
|
active: computed(() => model.value === item.value),
|
||||||
action: () => {
|
action: () => {
|
||||||
emit('update:modelValue', option.props?.value);
|
model.value = item.value as ModelTChecked;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
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, {
|
os.popupMenu(menu, container.value, {
|
||||||
|
|
|
@ -10,7 +10,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
v-panel
|
v-panel
|
||||||
:class="[$style.item, { [$style.itemWaiting]: item.preprocessing, [$style.itemCompleted]: item.uploaded, [$style.itemFailed]: item.uploadFailed }]"
|
:class="[$style.item, { [$style.itemWaiting]: item.preprocessing, [$style.itemCompleted]: item.uploaded, [$style.itemFailed]: item.uploadFailed }]"
|
||||||
:style="{ '--p': item.progress != null ? `${item.progress.value / item.progress.max * 100}%` : '0%' }"
|
:style="{
|
||||||
|
'--p': item.progress != null ? `${item.progress.value / item.progress.max * 100}%` : '0%',
|
||||||
|
'--pp': item.preprocessProgress != null ? `${item.preprocessProgress * 100}%` : '100%',
|
||||||
|
}"
|
||||||
@contextmenu.prevent.stop="onContextmenu(item, $event)"
|
@contextmenu.prevent.stop="onContextmenu(item, $event)"
|
||||||
>
|
>
|
||||||
<div :class="$style.itemInner">
|
<div :class="$style.itemInner">
|
||||||
|
@ -19,11 +22,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }" @click="onThumbnailClick(item, $event)"></div>
|
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }" @click="onThumbnailClick(item, $event)"></div>
|
||||||
<div :class="$style.itemBody">
|
<div :class="$style.itemBody">
|
||||||
<div><i v-if="item.isSensitive" style="color: var(--MI_THEME-warn); margin-right: 0.5em;" class="ti ti-eye-exclamation"></i><MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine></div>
|
<div>
|
||||||
|
<i v-if="item.isSensitive" style="color: var(--MI_THEME-warn); margin-right: 0.5em;" class="ti ti-eye-exclamation"></i>
|
||||||
|
<MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine>
|
||||||
|
</div>
|
||||||
<div :class="$style.itemInfo">
|
<div :class="$style.itemInfo">
|
||||||
<span>{{ item.file.type }}</span>
|
<span>{{ item.file.type }}</span>
|
||||||
<span v-if="item.compressedSize">({{ i18n.tsx._uploader.compressedToX({ x: bytes(item.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - item.compressedSize / item.file.size) * 100) }) }})</span>
|
<span v-if="item.compressedSize">({{ i18n.tsx._uploader.compressedToX({ x: bytes(item.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - item.compressedSize / item.file.size) * 100) }) }})</span>
|
||||||
<span v-else>{{ bytes(item.file.size) }}</span>
|
<span v-else>{{ bytes(item.file.size) }}</span>
|
||||||
|
<span v-if="item.preprocessing">{{ i18n.ts.preprocessing }}<MkLoading inline em style="margin-left: 0.5em;"/></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -97,7 +104,7 @@ function onThumbnailClick(item: UploaderItem, ev: MouseEvent) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: var(--pp, 100%);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(-45deg, transparent 25%, var(--c) 25%,var(--c) 50%, transparent 50%, transparent 75%, var(--c) 75%, var(--c));
|
background: linear-gradient(-45deg, transparent 25%, var(--c) 25%,var(--c) 50%, transparent 50%, transparent 75%, var(--c) 75%, var(--c));
|
||||||
background-size: 25px 25px;
|
background-size: 25px 25px;
|
||||||
|
|
|
@ -72,7 +72,7 @@ import { getStaticImageUrl } from '@/utility/media-proxy.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
showing: boolean;
|
showing: boolean;
|
||||||
q: string;
|
q: string | Misskey.entities.UserDetailed;
|
||||||
source: HTMLElement;
|
source: HTMLElement;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -99,10 +99,11 @@ async function fetchUser() {
|
||||||
user.value = props.q;
|
user.value = props.q;
|
||||||
error.value = false;
|
error.value = false;
|
||||||
} else {
|
} else {
|
||||||
const query: Omit<Misskey.entities.UsersShowRequest, 'userIds'> = props.q.startsWith('@') ?
|
const query: Misskey.entities.UsersShowRequest = props.q.startsWith('@') ?
|
||||||
Misskey.acct.parse(props.q.substring(1)) :
|
Misskey.acct.parse(props.q.substring(1)) :
|
||||||
{ userId: props.q };
|
{ userId: props.q };
|
||||||
|
|
||||||
|
// @ts-expect-error payloadの引数側の型が正常に解決されない
|
||||||
misskeyApi('users/show', query).then(res => {
|
misskeyApi('users/show', query).then(res => {
|
||||||
if (!props.showing) return;
|
if (!props.showing) return;
|
||||||
user.value = res;
|
user.value = res;
|
||||||
|
|
|
@ -18,6 +18,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
></MkPositionSelector>
|
></MkPositionSelector>
|
||||||
</FormSlot>
|
</FormSlot>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
:modelValue="layer.align.margin ?? 0"
|
||||||
|
:min="0"
|
||||||
|
:max="0.25"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
@update:modelValue="(v) => (layer as Extract<WatermarkPreset['layers'][number], { type: 'text' }>).align.margin = v"
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.margin }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
<MkRange
|
<MkRange
|
||||||
v-model="layer.scale"
|
v-model="layer.scale"
|
||||||
:min="0"
|
:min="0"
|
||||||
|
@ -66,6 +78,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
></MkPositionSelector>
|
></MkPositionSelector>
|
||||||
</FormSlot>
|
</FormSlot>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
:modelValue="layer.align.margin ?? 0"
|
||||||
|
:min="0"
|
||||||
|
:max="0.25"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
@update:modelValue="(v) => (layer as Extract<WatermarkPreset['layers'][number], { type: 'image' }>).align.margin = v"
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.margin }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
<MkRange
|
<MkRange
|
||||||
v-model="layer.scale"
|
v-model="layer.scale"
|
||||||
:min="0"
|
:min="0"
|
||||||
|
@ -107,6 +131,55 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="layer.type === 'qr'">
|
||||||
|
<MkInput v-model="layer.data" debounce>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.text }}</template>
|
||||||
|
<template #caption>{{ i18n.ts._watermarkEditor.leaveBlankToAccountUrl }}</template>
|
||||||
|
</MkInput>
|
||||||
|
|
||||||
|
<FormSlot>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.position }}</template>
|
||||||
|
<MkPositionSelector
|
||||||
|
v-model:x="layer.align.x"
|
||||||
|
v-model:y="layer.align.y"
|
||||||
|
></MkPositionSelector>
|
||||||
|
</FormSlot>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
:modelValue="layer.align.margin ?? 0"
|
||||||
|
:min="0"
|
||||||
|
:max="0.25"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
@update:modelValue="(v) => (layer as Extract<WatermarkPreset['layers'][number], { type: 'qr' }>).align.margin = v"
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.margin }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.scale"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.opacity"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else-if="layer.type === 'stripe'">
|
<template v-else-if="layer.type === 'stripe'">
|
||||||
<MkRange
|
<MkRange
|
||||||
v-model="layer.frequency"
|
v-model="layer.frequency"
|
||||||
|
|
|
@ -30,22 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.controls">
|
<div :class="$style.controls">
|
||||||
<div class="_spacer _gaps">
|
<div class="_spacer _gaps">
|
||||||
<MkSelect v-model="type" :items="[{ label: i18n.ts._watermarkEditor.text, value: 'text' }, { label: i18n.ts._watermarkEditor.image, value: 'image' }, { label: i18n.ts._watermarkEditor.advanced, value: 'advanced' }]">
|
<div class="_gaps_s">
|
||||||
<template #label>{{ i18n.ts._watermarkEditor.type }}</template>
|
|
||||||
</MkSelect>
|
|
||||||
|
|
||||||
<div v-if="type === 'text' || type === 'image'">
|
|
||||||
<XLayer
|
|
||||||
v-for="(layer, i) in preset.layers"
|
|
||||||
:key="layer.id"
|
|
||||||
v-model:layer="preset.layers[i]"
|
|
||||||
></XLayer>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="type === 'advanced'" class="_gaps_s">
|
|
||||||
<MkFolder v-for="(layer, i) in preset.layers" :key="layer.id" :defaultOpen="false" :canPage="false">
|
<MkFolder v-for="(layer, i) in preset.layers" :key="layer.id" :defaultOpen="false" :canPage="false">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div v-if="layer.type === 'text'">{{ i18n.ts._watermarkEditor.text }}</div>
|
<div v-if="layer.type === 'text'">{{ i18n.ts._watermarkEditor.text }}</div>
|
||||||
<div v-if="layer.type === 'image'">{{ i18n.ts._watermarkEditor.image }}</div>
|
<div v-if="layer.type === 'image'">{{ i18n.ts._watermarkEditor.image }}</div>
|
||||||
|
<div v-if="layer.type === 'qr'">{{ i18n.ts._watermarkEditor.qr }}</div>
|
||||||
<div v-if="layer.type === 'stripe'">{{ i18n.ts._watermarkEditor.stripe }}</div>
|
<div v-if="layer.type === 'stripe'">{{ i18n.ts._watermarkEditor.stripe }}</div>
|
||||||
<div v-if="layer.type === 'polkadot'">{{ i18n.ts._watermarkEditor.polkadot }}</div>
|
<div v-if="layer.type === 'polkadot'">{{ i18n.ts._watermarkEditor.polkadot }}</div>
|
||||||
<div v-if="layer.type === 'checker'">{{ i18n.ts._watermarkEditor.checker }}</div>
|
<div v-if="layer.type === 'checker'">{{ i18n.ts._watermarkEditor.checker }}</div>
|
||||||
|
@ -86,6 +76,7 @@ import * as os from '@/os.js';
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
import { ensureSignin } from '@/i.js';
|
import { ensureSignin } from '@/i.js';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
@ -94,7 +85,7 @@ function createTextLayer(): WatermarkPreset['layers'][number] {
|
||||||
id: genId(),
|
id: genId(),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: `(c) @${$i.username}`,
|
text: `(c) @${$i.username}`,
|
||||||
align: { x: 'right', y: 'bottom' },
|
align: { x: 'right', y: 'bottom', margin: 0 },
|
||||||
scale: 0.3,
|
scale: 0.3,
|
||||||
angle: 0,
|
angle: 0,
|
||||||
opacity: 0.75,
|
opacity: 0.75,
|
||||||
|
@ -108,7 +99,7 @@ function createImageLayer(): WatermarkPreset['layers'][number] {
|
||||||
type: 'image',
|
type: 'image',
|
||||||
imageId: null,
|
imageId: null,
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
align: { x: 'right', y: 'bottom' },
|
align: { x: 'right', y: 'bottom', margin: 0 },
|
||||||
scale: 0.3,
|
scale: 0.3,
|
||||||
angle: 0,
|
angle: 0,
|
||||||
opacity: 0.75,
|
opacity: 0.75,
|
||||||
|
@ -117,6 +108,17 @@ function createImageLayer(): WatermarkPreset['layers'][number] {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createQrLayer(): WatermarkPreset['layers'][number] {
|
||||||
|
return {
|
||||||
|
id: genId(),
|
||||||
|
type: 'qr',
|
||||||
|
data: '',
|
||||||
|
align: { x: 'right', y: 'bottom', margin: 0 },
|
||||||
|
scale: 0.3,
|
||||||
|
opacity: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function createStripeLayer(): WatermarkPreset['layers'][number] {
|
function createStripeLayer(): WatermarkPreset['layers'][number] {
|
||||||
return {
|
return {
|
||||||
id: genId(),
|
id: genId(),
|
||||||
|
@ -164,7 +166,7 @@ const props = defineProps<{
|
||||||
const preset = reactive<WatermarkPreset>(deepClone(props.preset) ?? {
|
const preset = reactive<WatermarkPreset>(deepClone(props.preset) ?? {
|
||||||
id: genId(),
|
id: genId(),
|
||||||
name: '',
|
name: '',
|
||||||
layers: [createTextLayer()],
|
layers: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -186,17 +188,6 @@ async function cancel() {
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = ref(preset.layers.length > 1 ? 'advanced' : preset.layers[0].type);
|
|
||||||
watch(type, () => {
|
|
||||||
if (type.value === 'text') {
|
|
||||||
preset.layers = [createTextLayer()];
|
|
||||||
} else if (type.value === 'image') {
|
|
||||||
preset.layers = [createImageLayer()];
|
|
||||||
} else if (type.value === 'advanced') {
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(preset, async (newValue, oldValue) => {
|
watch(preset, async (newValue, oldValue) => {
|
||||||
if (renderer != null) {
|
if (renderer != null) {
|
||||||
renderer.setLayers(preset.layers);
|
renderer.setLayers(preset.layers);
|
||||||
|
@ -326,6 +317,11 @@ function addLayer(ev: MouseEvent) {
|
||||||
action: () => {
|
action: () => {
|
||||||
preset.layers.push(createImageLayer());
|
preset.layers.push(createImageLayer());
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
text: i18n.ts._watermarkEditor.qr,
|
||||||
|
action: () => {
|
||||||
|
preset.layers.push(createQrLayer());
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
text: i18n.ts._watermarkEditor.stripe,
|
text: i18n.ts._watermarkEditor.stripe,
|
||||||
action: () => {
|
action: () => {
|
||||||
|
|
|
@ -7,9 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<template v-if="edit">
|
<template v-if="edit">
|
||||||
<header :class="$style.editHeader">
|
<header :class="$style.editHeader">
|
||||||
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
|
<MkSelect v-model="widgetAdderSelected" :items="widgetAdderSelectedDef" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
|
||||||
<template #label>{{ i18n.ts.selectWidget }}</template>
|
<template #label>{{ i18n.ts.selectWidget }}</template>
|
||||||
<option v-for="widget in _widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||||
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
|
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||||
|
@ -59,6 +58,7 @@ import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||||
|
|
||||||
|
@ -89,7 +89,15 @@ const widgetRefs = {};
|
||||||
const configWidget = (id: string) => {
|
const configWidget = (id: string) => {
|
||||||
widgetRefs[id].configure();
|
widgetRefs[id].configure();
|
||||||
};
|
};
|
||||||
const widgetAdderSelected = ref<string | null>(null);
|
|
||||||
|
const {
|
||||||
|
model: widgetAdderSelected,
|
||||||
|
def: widgetAdderSelectedDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: computed(() => [{ label: i18n.ts.none, value: null }, ..._widgetDefs.value.map(x => ({ label: i18n.ts._widgets[x], value: x }))]),
|
||||||
|
initialValue: null,
|
||||||
|
});
|
||||||
|
|
||||||
const addWidget = () => {
|
const addWidget = () => {
|
||||||
if (widgetAdderSelected.value == null) return;
|
if (widgetAdderSelected.value == null) return;
|
||||||
|
|
||||||
|
|
|
@ -4,31 +4,39 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.root, { [$style.inline]: inline }]">
|
<component
|
||||||
<a v-if="external" :class="$style.main" class="_button" :href="to" target="_blank">
|
:is="to ? 'div' : 'button'"
|
||||||
|
:class="[
|
||||||
|
$style.root,
|
||||||
|
{
|
||||||
|
[$style.inline]: inline,
|
||||||
|
'_button': !to,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="to ? (external ? 'a' : 'MkA') : 'div'"
|
||||||
|
:class="[$style.main, { [$style.active]: active }]"
|
||||||
|
class="_button"
|
||||||
|
v-bind="to ? (external ? { href: to, target: '_blank' } : { to, behavior }) : {}"
|
||||||
|
>
|
||||||
<span :class="$style.icon"><slot name="icon"></slot></span>
|
<span :class="$style.icon"><slot name="icon"></slot></span>
|
||||||
<span :class="$style.text"><slot></slot></span>
|
<div :class="$style.headerText">
|
||||||
|
<div>
|
||||||
|
<MkCondensedLine :minScale="2 / 3"><slot></slot></MkCondensedLine>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<span :class="$style.suffix">
|
<span :class="$style.suffix">
|
||||||
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
|
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
|
||||||
<i class="ti ti-external-link"></i>
|
<i :class="to && external ? 'ti ti-external-link' : 'ti ti-chevron-right'"></i>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</component>
|
||||||
<MkA v-else :class="[$style.main, { [$style.active]: active }]" class="_button" :to="to" :behavior="behavior">
|
</component>
|
||||||
<span :class="$style.icon"><slot name="icon"></slot></span>
|
|
||||||
<span :class="$style.text"><slot></slot></span>
|
|
||||||
<span :class="$style.suffix">
|
|
||||||
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
|
|
||||||
<i class="ti ti-chevron-right"></i>
|
|
||||||
</span>
|
|
||||||
</MkA>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
defineProps<{
|
||||||
|
to?: string;
|
||||||
const props = defineProps<{
|
|
||||||
to: string;
|
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
external?: boolean;
|
external?: boolean;
|
||||||
behavior?: null | 'window' | 'browser';
|
behavior?: null | 'window' | 'browser';
|
||||||
|
@ -75,17 +83,18 @@ const props = defineProps<{
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
& + .text {
|
& + .headerText {
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.headerText {
|
||||||
flex-shrink: 1;
|
white-space: nowrap;
|
||||||
white-space: normal;
|
text-overflow: ellipsis;
|
||||||
|
text-align: start;
|
||||||
|
overflow: hidden;
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.suffix {
|
.suffix {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||||
import MkAd from './MkAd.vue';
|
import MkAd from './MkAd.vue';
|
||||||
import type { StoryObj } from '@storybook/vue3';
|
import type { StoryObj } from '@storybook/vue3';
|
||||||
|
@ -75,6 +75,7 @@ const common = {
|
||||||
place: '',
|
place: '',
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
dayOfWeek: 7,
|
dayOfWeek: 7,
|
||||||
|
isSensitive: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
|
|
|
@ -14,9 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import isChromatic from 'chromatic/isChromatic';
|
import isChromatic from 'chromatic/isChromatic';
|
||||||
import { onMounted, onUnmounted, ref, computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { dateTimeFormat } from '@@/js/intl-const.js';
|
import { dateTimeFormat } from '@@/js/intl-const.js';
|
||||||
|
import { useLowresTime } from '@/composables/use-lowres-time.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
time: Date | string | number | null;
|
time: Date | string | number | null;
|
||||||
|
@ -46,8 +47,10 @@ const _time = props.time == null ? NaN : getDateSafe(props.time).getTime();
|
||||||
const invalid = Number.isNaN(_time);
|
const invalid = Number.isNaN(_time);
|
||||||
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
||||||
|
|
||||||
|
const actualNow = useLowresTime();
|
||||||
|
const now = computed(() => (props.origin ? props.origin.getTime() : actualNow.value));
|
||||||
|
|
||||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||||
const now = ref(props.origin?.getTime() ?? Date.now());
|
|
||||||
const ago = computed(() => (now.value - _time) / 1000/*ms*/);
|
const ago = computed(() => (now.value - _time) / 1000/*ms*/);
|
||||||
|
|
||||||
const relative = computed<string>(() => {
|
const relative = computed<string>(() => {
|
||||||
|
@ -72,29 +75,6 @@ const relative = computed<string>(() => {
|
||||||
i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() })
|
i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let tickId: number;
|
|
||||||
let currentInterval: number;
|
|
||||||
|
|
||||||
function tick() {
|
|
||||||
now.value = Date.now();
|
|
||||||
const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000;
|
|
||||||
|
|
||||||
if (currentInterval !== nextInterval) {
|
|
||||||
if (tickId) window.clearInterval(tickId);
|
|
||||||
currentInterval = nextInterval;
|
|
||||||
tickId = window.setInterval(tick, nextInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!invalid && props.origin === null && (props.mode === 'relative' || props.mode === 'detail')) {
|
|
||||||
onMounted(() => {
|
|
||||||
tick();
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (tickId) window.clearInterval(tickId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkPageHeader v-else v-model:tab="tab" v-bind="pageHeaderProps"/>
|
<MkPageHeader v-else v-model:tab="tab" v-bind="pageHeaderProps"/>
|
||||||
</template>
|
</template>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<MkSwiper v-if="prefer.s.enableHorizontalSwipe && swipable && (props.tabs?.length ?? 1) > 1" v-model:tab="tab" :class="$style.swiper" :tabs="props.tabs">
|
<MkSwiper v-if="prefer.s.enableHorizontalSwipe && swipable && (props.tabs?.length ?? 1) > 1" v-model:tab="tab" :class="$style.swiper" :tabs="props.tabs ?? []">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</MkSwiper>
|
</MkSwiper>
|
||||||
<slot v-else></slot>
|
<slot v-else></slot>
|
||||||
|
@ -45,7 +45,7 @@ const props = withDefaults(defineProps<PageHeaderProps & {
|
||||||
});
|
});
|
||||||
|
|
||||||
const pageHeaderProps = computed(() => {
|
const pageHeaderProps = computed(() => {
|
||||||
const { reversed, ...rest } = props;
|
const { reversed, tab, ...rest } = props;
|
||||||
return rest;
|
return rest;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -75,10 +75,6 @@ defineExpose({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.body, .swiper {
|
.body, .swiper {
|
||||||
min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
|
min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue