Merge branch 'develop' into img-max
This commit is contained in:
commit
c9dd511d64
46
CHANGELOG.md
46
CHANGELOG.md
|
@ -12,6 +12,52 @@
|
|||
|
||||
-->
|
||||
|
||||
## 13.x.x (unreleased)
|
||||
|
||||
### General
|
||||
- カスタム絵文字関連の変更
|
||||
* ノートなどに含まれるemojis(populateEmojiの結果)は(プロキシされたURLではなく)オリジナルのURLを指すように
|
||||
* MFMでx3/x4もしくはscale.x/yが2.5以上に指定されていた場合にはオリジナル品質の絵文字を使用するように
|
||||
|
||||
### Client
|
||||
-
|
||||
|
||||
### Server
|
||||
- ノート作成時のアンテナ追加パフォーマンスを改善
|
||||
- フォローインポートなどでの大量のフォロー等操作をキューイングするように #10544 @nmkj-io
|
||||
|
||||
## 13.11.2
|
||||
|
||||
### Note
|
||||
- 13.11.0または13.11.1から13.11.2以降にアップデートする場合、Redisのカスタム絵文字のキャッシュを削除する必要があります(https://github.com/misskey-dev/misskey/issues/10502#issuecomment-1502790755 参照)
|
||||
|
||||
### General
|
||||
- チャンネルの検索用ページの追加
|
||||
|
||||
### Client
|
||||
- 常に広告を見られるオプションを追加
|
||||
- ユーザーページの画像一覧が表示されない問題を修正
|
||||
- webhook, 連携アプリ一覧でコンテンツが重複して表示される問題を修正
|
||||
- iPhoneで絵文字ピッカーの表示が崩れる問題を修正
|
||||
- iPhoneでウィジェットドロワーの「ウィジェットを編集」が押しにくい問題を修正
|
||||
- 投稿フォームのデザインを調整
|
||||
- ギャラリーの人気の投稿が無限にページングされる問題を修正
|
||||
|
||||
### Server
|
||||
- channels/search Endpoint APIの追加
|
||||
- APIパラメータサイズ上限を32kbから1mbに緩和
|
||||
- プッシュ通知送信時のパフォーマンスを改善
|
||||
- ローカルのカスタム絵文字のキャッシュが効いていなかった問題を修正
|
||||
- アンテナのノート、チャンネルのノート、通知が正常に作成できないことがある問題を修正
|
||||
- ストリーミングのLTLチャンネルでサーバー側にエラーログが出るのを修正
|
||||
|
||||
### Service Worker
|
||||
- 「通知が既読になったらプッシュ通知を削除する」を復活
|
||||
* 「プッシュ通知が更新されました」の挙動を変えた(ホストとバージョンを表示するようにし、一定時間後の削除は行わないように)
|
||||
- プッシュ通知が実績を解除 (achievementEarned) に対応
|
||||
- プッシュ通知のアクションから既存のクライアントの投稿フォームを開くことになった際の挙動を修正
|
||||
- たくさんのプッシュ通知を閉じた際、その通知の数だけnotifications/mark-all-as-readを叩くのをやめるように
|
||||
|
||||
## 13.11.1
|
||||
|
||||
### General
|
||||
|
|
|
@ -991,6 +991,7 @@ largeNoteReactions: "Reaktionen vergrößert anzeigen"
|
|||
noteIdOrUrl: "Notiz-ID oder URL"
|
||||
accountMigration: "Konto-Umzug"
|
||||
accountMoved: "Dieser Benutzer ist zu einem neuen Konto umgezogen:"
|
||||
forceShowAds: "Werbung immer anzeigen"
|
||||
_accountMigration:
|
||||
moveTo: "Dieses Konto zu einem neuen umziehen"
|
||||
moveToLabel: "Umzugsziel:"
|
||||
|
|
|
@ -67,7 +67,7 @@ import: "Import"
|
|||
export: "Export"
|
||||
files: "Files"
|
||||
download: "Download"
|
||||
driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? All notes with this file attached will also be deleted."
|
||||
driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? It will also vanish from all contents that use it."
|
||||
unfollowConfirm: "Are you sure you want to unfollow {name}?"
|
||||
exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed."
|
||||
importRequested: "You've requested an import. This may take a while."
|
||||
|
@ -500,7 +500,7 @@ objectStoragePrefixDesc: "Files will be stored under directories with this prefi
|
|||
objectStorageEndpoint: "Endpoint"
|
||||
objectStorageEndpointDesc: "Leave this empty if you are using AWS S3, otherwise specify the endpoint as '<host>' or '<host>:<port>', depending on the service you are using."
|
||||
objectStorageRegion: "Region"
|
||||
objectStorageRegionDesc: "Specify a region like 'xx-east-1'. If your service does not distinguish between regions, leave this blank or enter 'us-east-1'."
|
||||
objectStorageRegionDesc: "Specify a region like 'xx-east-1'. If your service does not distinguish between regions, enter 'us-east-1'. Leave empty if using AWS configuration files or environment variables."
|
||||
objectStorageUseSSL: "Use SSL"
|
||||
objectStorageUseSSLDesc: "Turn this off if you are not going to use HTTPS for API connections"
|
||||
objectStorageUseProxy: "Connect over Proxy"
|
||||
|
@ -918,7 +918,7 @@ unsubscribePushNotification: "Disable push notifications"
|
|||
pushNotificationAlreadySubscribed: "Push notifications are already enabled"
|
||||
pushNotificationNotSupported: "Your browser or instance does not support push notifications"
|
||||
sendPushNotificationReadMessage: "Delete push notifications once the relevant notifications or messages have been read"
|
||||
sendPushNotificationReadMessageCaption: "A notification containing the text \"{emptyPushNotificationMessage}\" will be displayed for a short time. This may increase the battery usage of your device, if applicable."
|
||||
sendPushNotificationReadMessageCaption: "A notification containing the text \"{emptyPushNotificationMessage}\" will be displayed for a short time. This may increase the power consumption of your device."
|
||||
windowMaximize: "Maximize"
|
||||
windowMinimize: "Minimize"
|
||||
windowRestore: "Restore"
|
||||
|
@ -991,6 +991,7 @@ largeNoteReactions: "Enlargen displayed reactions"
|
|||
noteIdOrUrl: "Note ID or URL"
|
||||
accountMigration: "Account Migration"
|
||||
accountMoved: "This user has moved to a new account:"
|
||||
forceShowAds: "Always show ads"
|
||||
_accountMigration:
|
||||
moveTo: "Migrate this account to a different one"
|
||||
moveToLabel: "Account to move to:"
|
||||
|
@ -1868,7 +1869,7 @@ _deck:
|
|||
swapRight: "Swap with the right column"
|
||||
swapUp: "Swap with the above column"
|
||||
swapDown: "Swap with the below column"
|
||||
stackLeft: "Stack with the left column"
|
||||
stackLeft: "Stack on left column"
|
||||
popRight: "Pop column to the right"
|
||||
profile: "Profile"
|
||||
newProfile: "New profile"
|
||||
|
|
|
@ -122,6 +122,8 @@ unmarkAsSensitive: "Hapus tanda konten sensitif"
|
|||
enterFileName: "Masukkan nama berkas"
|
||||
mute: "Bisukan"
|
||||
unmute: "Hapus bisukan"
|
||||
renoteMute: "Matikan renote"
|
||||
renoteUnmute: "Batal mematikan renote"
|
||||
block: "Blokir"
|
||||
unblock: "Buka blokir"
|
||||
suspend: "Bekukan"
|
||||
|
@ -393,11 +395,15 @@ about: "Informasi"
|
|||
aboutMisskey: "Tentang Misskey"
|
||||
administrator: "Admin"
|
||||
token: "Token"
|
||||
totp: "Aplikasi autentikator"
|
||||
totpDescription: "Gunakan aplikasi autentikator untuk mendapatkan kata sandi sekali pakai"
|
||||
moderator: "Moderator"
|
||||
moderation: "Moderasi"
|
||||
nUsersMentioned: "{n} pengguna disebut"
|
||||
securityKeyAndPasskey: "Security key dan passkey"
|
||||
securityKey: "Kunci keamanan"
|
||||
lastUsed: "Terakhir digunakan"
|
||||
lastUsedAt: "Penggunaan terakhir: {t}"
|
||||
unregister: "Batalkan pendaftaran"
|
||||
passwordLessLogin: "Setel login tanpa kata sandi"
|
||||
resetPassword: "Atur ulang kata sandi"
|
||||
|
@ -844,6 +850,7 @@ tenMinutes: "10 Menit"
|
|||
oneHour: "1 Jam"
|
||||
oneDay: "1 Hari"
|
||||
oneWeek: "1 Bulan"
|
||||
oneMonth: "satu bulan"
|
||||
reflectMayTakeTime: "Mungkin perlu beberapa saat untuk dicerminkan."
|
||||
failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun"
|
||||
rateLimitExceeded: "Batas sudah terlampaui"
|
||||
|
@ -901,6 +908,7 @@ pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung pember
|
|||
sendPushNotificationReadMessage: "Hapus pemberitahuan push ketika pemberitahuan relevan atau pesan telah dibaca"
|
||||
sendPushNotificationReadMessageCaption: "Pemberitahuan berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu."
|
||||
windowMaximize: "Maksimalkan"
|
||||
windowMinimize: "Minimalkan"
|
||||
windowRestore: "Kembalikan"
|
||||
caption: "Keterangan"
|
||||
loggedInAsBot: "Sedang login sebagai bot"
|
||||
|
@ -939,6 +947,12 @@ collapseRenotes: "Tutup renote yang sudah kamu lihat"
|
|||
internalServerError: "Kesalahan internal peladen"
|
||||
internalServerErrorDescription: "Peladen sedang mengalami galat tak terduga"
|
||||
copyErrorInfo: "Salin detil galat"
|
||||
joinThisServer: "Gabung server ini"
|
||||
exploreOtherServers: "Cari server lain"
|
||||
letsLookAtTimeline: "LIhat timeline"
|
||||
disableFederationConfirm: "Matikan federasi?"
|
||||
disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi privat. Umumnya, mematikan federasi tidak diperlukan."
|
||||
disableFederationOk: "Matikan federasi"
|
||||
_achievements:
|
||||
earnedAt: "Terbuka pada"
|
||||
_types:
|
||||
|
|
|
@ -20,6 +20,7 @@ noNotes: "ノートはありません"
|
|||
noNotifications: "通知はありません"
|
||||
instance: "サーバー"
|
||||
settings: "設定"
|
||||
notificationSettings: "通知の設定"
|
||||
basicSettings: "基本設定"
|
||||
otherSettings: "その他の設定"
|
||||
openInWindow: "ウィンドウで開く"
|
||||
|
@ -920,8 +921,8 @@ subscribePushNotification: "プッシュ通知を有効化"
|
|||
unsubscribePushNotification: "プッシュ通知を停止する"
|
||||
pushNotificationAlreadySubscribed: "プッシュ通知は有効です"
|
||||
pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に非対応"
|
||||
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
|
||||
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
|
||||
sendPushNotificationReadMessage: "通知が既読になったらプッシュ通知を削除する"
|
||||
sendPushNotificationReadMessageCaption: "端末の電池消費量が増加する可能性があります。"
|
||||
windowMaximize: "最大化"
|
||||
windowMinimize: "最小化"
|
||||
windowRestore: "元に戻す"
|
||||
|
@ -994,6 +995,7 @@ largeNoteReactions: "ノートのリアクションを大きく表示"
|
|||
noteIdOrUrl: "ノートIDまたはURL"
|
||||
accountMigration: "アカウントの引っ越し"
|
||||
accountMoved: "このユーザーは新しいアカウントに引っ越しました:"
|
||||
forceShowAds: "常に広告を表示する"
|
||||
|
||||
_accountMigration:
|
||||
moveTo: "このアカウントを新しいアカウントに引っ越す"
|
||||
|
@ -1429,6 +1431,8 @@ _channel:
|
|||
following: "フォロー中"
|
||||
usersCount: "{n}人が参加中"
|
||||
notesCount: "{n}投稿があります"
|
||||
nameAndDescription: "名前と説明"
|
||||
nameOnly: "名前のみ"
|
||||
|
||||
_menuDisplay:
|
||||
sideFull: "横"
|
||||
|
|
|
@ -991,6 +991,7 @@ largeNoteReactions: "ノートのリアクションを大きする"
|
|||
noteIdOrUrl: "ノートIDかURL"
|
||||
accountMigration: "アカウントのお引っ越し"
|
||||
accountMoved: "このユーザーはさらのアカウントに引っ越したで:"
|
||||
forceShowAds: "常に広告を表示しとく"
|
||||
_accountMigration:
|
||||
moveTo: "このアカウントをさらのアカウントに引っ越すで"
|
||||
moveToLabel: "引っ越し先のアカウント:"
|
||||
|
|
|
@ -163,11 +163,15 @@ instanceInfo: "ອີນສະແຕນ"
|
|||
statistics: "ສະຖິຕິ"
|
||||
clearQueue: "ລ້າງຄິວ"
|
||||
clearCachedFiles: "ລຶບລ້າງແຄສ"
|
||||
noUsers: "ບໍ່ພົບຜູ້ໃຊ້"
|
||||
editProfile: "ແກ້ໄຂໂປຣໄຟລ໌"
|
||||
done: "ສຳເລັດ"
|
||||
processing: "ກຳລັງປະມວນຜົນ"
|
||||
preview: "ສະແດງເປັນຕົວຢ່າງ"
|
||||
default: "ຄ່າເລີ່ມຕົ້ນ"
|
||||
defaultValueIs: "ຄ່າເລີ່ມຕົ້ນ: {value}"
|
||||
noCustomEmojis: "ບໍ່ມີອີໂມຈິ"
|
||||
noJobs: "ບໍ່ມີຊິ້ນວຽກ"
|
||||
federating: "ສະຫະພັນ"
|
||||
blocked: "ບລັອກແລ້ວ "
|
||||
suspended: "ໂຈະ"
|
||||
|
@ -182,6 +186,9 @@ changePassword: "ປ່ຽນລະຫັດຜ່ານ"
|
|||
security: "ຄວາມປອດໄພ"
|
||||
retypedNotMatch: "ວັດສະດຸປ້ອນບໍ່ກົງກັນ"
|
||||
currentPassword: "ລະຫັດຜ່ານປະຈຸບັນ"
|
||||
newPassword: "ລະຫັດຜ່ານໃໝ່"
|
||||
newPasswordRetype: "ໃສ່ລະຫັດຜ່ານໃໝ່ອີກເທື່ອໜຶ່ງ"
|
||||
attachFile: "ແນບໄຟລ໌"
|
||||
more: "ເພີ່ມເຕີມ!"
|
||||
featured: "ໄຮໄລທ໌"
|
||||
usernameOrUserId: "ຊື່ຜູ້ໃຊ້ ຫຼື id ຜູ້ໃຊ້"
|
||||
|
@ -196,25 +203,31 @@ saved: "ບັນທຶກແລ້ວ"
|
|||
messaging: "ແຊ໋ດ"
|
||||
upload: "ອັບໂຫຼດ"
|
||||
keepOriginalUploading: "ຮັກສາຮູບພາບຕົ້ນສະບັບ"
|
||||
fromDrive: "ຈາກ Drive"
|
||||
fromUrl: "ຈາກ URL"
|
||||
uploadFromUrl: "ອັບໂຫຼດຈາກ URL"
|
||||
uploadFromUrlDescription: "URL ຂອງໄຟລ໌ທີ່ທ່ານຕ້ອງການອັບໂຫລດ"
|
||||
uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດ"
|
||||
messageRead: "ອ່ານແລ້ວ"
|
||||
startMessaging: "ເລີ່ມການສົນທະນາໃໝ່"
|
||||
nUsersRead: "ອ່ານໂດຍ {n}"
|
||||
tos: "ເງື່ອນໄຂການໃຫ້ບໍລິການ"
|
||||
start: "ເລີ່ມຕົ້ນນຳໃຊ້ເລີຍ"
|
||||
home: "ໜ້າຫຼັກ"
|
||||
activity: "ກິດຈະກຳ"
|
||||
images: "ຮູບພາບ"
|
||||
birthday: "ວັນເກີດ"
|
||||
yearsOld: "{age} ປີ"
|
||||
registeredDate: "ວັນທີ່ເປັນສະມາຊິກ"
|
||||
location: "ທີ່ຕັ້ງ"
|
||||
theme: "ແທ໋ມ"
|
||||
themeForLightMode: "ຮູບແບບສີສັນເພື່ອໃຊ້ໃນໂໝດແສງ"
|
||||
themeForDarkMode: "ຮູບແບບສີສັນທີ່ຈະໃຊ້ຢູ່ໃນໂໝດມືດ"
|
||||
light: "ສະຫວ່າງ"
|
||||
dark: "ມືດ"
|
||||
lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ"
|
||||
darkThemes: "ຮູບແບບສີສັນມືດ"
|
||||
syncDeviceDarkMode: "ຊິງຄ໌ໂໝດມືດກັບການຕັ້ງຄ່າທົ່ວອຸປະກອນ"
|
||||
drive: "ຂັບ"
|
||||
fileName: "ຊື່ໄຟລ໌"
|
||||
selectFile: "ເລືອກໄຟລ໌"
|
||||
|
@ -265,6 +278,9 @@ invite: "ເຊີນ"
|
|||
driveCapacityPerLocalAccount: "ຄວາມອາດສາມາດຂັບຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ"
|
||||
driveCapacityPerRemoteAccount: "ໄດຣຟ໌ຄວາມອາດສາມາດຕໍ່ຜູ້ໃຊ້ທາງໄກ"
|
||||
pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້"
|
||||
turnstileSiteKey: "ກະແຈໄຊທ໌"
|
||||
turnstileSecretKey: "ກະແຈລັບ"
|
||||
name: "ຊື່"
|
||||
userList: "ລາຍການ"
|
||||
about: "ກ່ຽວກັບ"
|
||||
aboutMisskey: "ກ່ຽວກັບ Misskey"
|
||||
|
@ -326,6 +342,7 @@ _widgets:
|
|||
instanceInfo: "ອີນສະແຕນ"
|
||||
notifications: "ການແຈ້ງເຕືອນ"
|
||||
timeline: "ເສັ້ນກຳນົດເວລາ"
|
||||
activity: "ກິດຈະກຳ"
|
||||
federation: "ສະຫະພັນ"
|
||||
_userList:
|
||||
chooseList: "ເລືອກບັນຊີລາຍການ"
|
||||
|
@ -335,6 +352,7 @@ _visibility:
|
|||
home: "ໜ້າຫຼັກ"
|
||||
followers: "ຜູ້ຕິດຕາມ"
|
||||
_profile:
|
||||
name: "ຊື່"
|
||||
username: "ຊື່ຜູ້ໃຊ້"
|
||||
_exportOrImport:
|
||||
followingList: "ກຳລັງຕິດຕາມ"
|
||||
|
@ -368,3 +386,5 @@ _deck:
|
|||
list: "ລາຍການ"
|
||||
channel: "ຊ່ອງ"
|
||||
mentions: "ກ່າວເຖິງ"
|
||||
_webhookSettings:
|
||||
name: "ຊື່"
|
||||
|
|
|
@ -122,6 +122,8 @@ unmarkAsSensitive: "ยกเลิกทำเครื่องหมายเ
|
|||
enterFileName: "พิมพ์ชื่อไฟล์"
|
||||
mute: "ปิดเสียง"
|
||||
unmute: "ยกเลิกการปิดเสียง"
|
||||
renoteMute: "ปิดเสียงรีโน้ต"
|
||||
renoteUnmute: "เปิดเสียง รีโน้ต"
|
||||
block: "บล็อค"
|
||||
unblock: "เลิกปิดกั้น"
|
||||
suspend: "ถูกระงับ"
|
||||
|
@ -153,6 +155,7 @@ flagShowTimelineReplies: "แสดงตอบกลับ ในไทม์
|
|||
flagShowTimelineRepliesDescription: "แสดงการตอบกลับของผู้ใช้งานไปยังโน้ตของผู้ใช้งานรายอื่นๆในไทม์ไลน์หากได้เปิดเอาไว้"
|
||||
autoAcceptFollowed: "อนุมัติคำขอติดตามโดยอัตโนมัติทันที จากผู้ใช้งานที่คุณกำลังติดตาม"
|
||||
addAccount: "เพิ่มบัญชี"
|
||||
reloadAccountsList: "รีโหลดรายการบัญชีใหม่"
|
||||
loginFailed: "การเข้าสู่ระบบไม่สำเร็จ"
|
||||
showOnRemote: "ดูบนอินสแตนซ์ระยะไกล"
|
||||
general: "ทั่วไป"
|
||||
|
@ -503,6 +506,7 @@ objectStorageUseSSLDesc: "ปิดการทำงานนี้ไว้
|
|||
objectStorageUseProxy: "เชื่อมต่อผ่านพร็อกซี"
|
||||
objectStorageUseProxyDesc: "ปิดสิ่งนี้ไว้ถ้าหากคุณจะไม่ใช้ Proxy สำหรับการเชื่อมต่อ API"
|
||||
objectStorageSetPublicRead: "ตั้งค่า \"public-read\" ในการอัปโหลด"
|
||||
s3ForcePathStyleDesc: "ถ้าหากเปิดใช้งาน s3ForcePathStyle ชื่อบัคเก็ตนั้นอาจจะต้องรวมอยู่ในเส้นทางของ URL ซึ่งตรงข้ามกับชื่อโฮสต์ของ URL คุณอาจจะต้องเปิดใช้งานการตั้งค่านี้เมื่อใช้บริการต่างๆ เช่น อินสแตนซ์ Minio ที่โฮสต์เองนะ"
|
||||
serverLogs: "บันทึกของเซิร์ฟเวอร์"
|
||||
deleteAll: "ลบทั้งหมด"
|
||||
showFixedPostForm: "แสดงแบบฟอร์มการโพสต์ที่ด้านบนสุดของไทม์ไลน์"
|
||||
|
@ -545,7 +549,9 @@ userSilenced: "ผู้ใช้รายนี้กำลังถูกป
|
|||
yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
|
||||
yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
|
||||
tokenRevoked: "โทเค็นไม่ถูกต้อง"
|
||||
tokenRevokedDescription: "โทเค็นนี้หมดอายุแล้วนะค่ะกรุณาเข้าสู่ระบบอีกครั้งนะ"
|
||||
accountDeleted: "ลบบัญชีแล้ว"
|
||||
accountDeletedDescription: "บัญชีนี้ถูกลบไปแล้วนะ"
|
||||
menu: "เมนู"
|
||||
divider: "ตัวแบ่ง"
|
||||
addItem: "เพิ่มรายการ"
|
||||
|
@ -914,6 +920,7 @@ pushNotificationNotSupported: "เบราว์เซอร์หรืออ
|
|||
sendPushNotificationReadMessage: "ลบการแจ้งเตือนแบบพุชเมื่ออ่านการแจ้งเตือนหรือข้อความที่เกี่ยวข้องแล้ว"
|
||||
sendPushNotificationReadMessageCaption: "การแจ้งเตือนที่มีข้อความ \"{emptyPushNotificationMessage}\" จะแสดงขึ้นมาในช่วงระยะเวลาสั้นๆ การดำเนินการนี้อาจทำให้เพิ่มการใช้งานแบตเตอรี่ของอุปกรณ์ถ้าหากมีนะ"
|
||||
windowMaximize: "ขยายใหญ่สุดแล้ว"
|
||||
windowMinimize: "ย่อเล็กที่สุด"
|
||||
windowRestore: "เลิกทำ"
|
||||
caption: "รายละเอียด"
|
||||
loggedInAsBot: "ล็อกอินเป็นบอตอยู่ในขณะนี้"
|
||||
|
@ -955,11 +962,17 @@ copyErrorInfo: "คัดลอกรายละเอียดข้อผิ
|
|||
joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้"
|
||||
exploreOtherServers: "มองหาอินสแตนซ์อื่น"
|
||||
letsLookAtTimeline: "ลองดูที่ไทม์ไลน์"
|
||||
disableFederationConfirm: "ปิดใช้งานสหพันธ์จริงๆหรอแน่ใจแล้วนะ?"
|
||||
disableFederationConfirmWarn: "แม้ว่าจะถูกยกเลิกเอาไว้โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป เว้นแต่ว่า...จะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องทำตรงนี้หรอกนะค่ะ"
|
||||
disableFederationOk: "ปิดการใช้งาน"
|
||||
invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ"
|
||||
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
|
||||
postToTheChannel: "โพสต์ลงช่อง"
|
||||
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
|
||||
reactionAcceptance: "การยอมรับรีแอคชั่น"
|
||||
likeOnly: "ที่ชอบเท่านั้น"
|
||||
likeOnlyForRemote: "ไลค์สำหรับอินสแตนซ์ระยะไกลเท่านั้น"
|
||||
rolesAssignedToMe: "บทบาทที่ได้รับมอบหมายให้ฉัน"
|
||||
resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?"
|
||||
sensitiveWords: "คำที่ละเอียดอ่อน"
|
||||
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
|
||||
|
@ -971,6 +984,22 @@ drivecleaner: "ทำความสะอาดไดรฟ์"
|
|||
retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง"
|
||||
retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
|
||||
retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
|
||||
enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล"
|
||||
enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล"
|
||||
showClipButtonInNoteFooter: "เพิ่ม \"คลิป\" เพื่อบันทึกเมนูการทำงาน"
|
||||
largeNoteReactions: "ขยายรีแอคชั่นการแสดงผล"
|
||||
noteIdOrUrl: "โน้ต ID หรือ URL"
|
||||
accountMigration: "การโยกย้ายบัญชี"
|
||||
accountMoved: "ผู้ใช้รายนี้ได้ย้ายไปยังบัญชีใหม่แล้ว:"
|
||||
forceShowAds: "แสดงโฆษณาเสมอ"
|
||||
_accountMigration:
|
||||
moveTo: "ย้ายข้อมูลบัญชีนี้ไปยังบัญชีอีกหนึ่ง"
|
||||
moveToLabel: "บัญชีที่จะย้ายไปที่:"
|
||||
moveAccountDescription: "การกระทำนี้ไม่สามารถย้อนกลับได้นะ ขั้นตอนแรก ต้องสร้างนามแฝงสำหรับบัญชีนี้ในบัญชีที่คุณต้องการย้ายไป หลังจากนั้นแล้ว ป้อนบัญชีที่จะย้ายไปในรูปแบบดังต่อไปนี้: @person@instance.com"
|
||||
moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง"
|
||||
moveFromLabel: "บัญชีที่จะย้ายจาก:"
|
||||
moveFromDescription: "สร้างนามแฝงสำหรับบัญชีที่จะย้ายจากบัญชีนี้ ถ้าหากคุณต้องการโอนผู้ติดตาม สิ่งนี้ต้องทำก่อนโอนก่อนนะค่ะ! หลังจากนั้น ป้อนบัญชีที่จะย้ายไปในรูปแบบต่อไปนี้: @person@instance.com"
|
||||
migrationConfirm: "ย้ายข้อมูลบัญชีนี้ไปที่ {account} จริงๆนะ เมื่อมีการเริ่มต้นแล้ว กระบวนการนี้จะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ เพื่อให้แน่ใจยืนยันว่าคุณได้สร้างนามแฝงในบัญชีที่จะย้ายข้อมูลนะค่ะ"
|
||||
_achievements:
|
||||
earnedAt: "ได้รับเมื่อ"
|
||||
_types:
|
||||
|
@ -1267,6 +1296,8 @@ _role:
|
|||
followersMoreThanOrEq: "จำนวนผู้ติดตามมากกว่าหรือเท่ากับ\n"
|
||||
followingLessThanOrEq: "จำนวนบัญชีต่อไปนี้คือ น้อยกว่าหรือเท่ากับ"
|
||||
followingMoreThanOrEq: "จำนวนบัญชีต่อไปนี้คือ มากกว่าหรือเท่ากับ"
|
||||
notesLessThanOrEq: "จำนวนโพสต์น้อยกว่าเท่ากับ"
|
||||
notesMoreThanOrEq: "จำนวนโพสต์มากกว่าเท่ากับ"
|
||||
and: "และ"
|
||||
or: "หรือ"
|
||||
not: "ไม่"
|
||||
|
@ -1866,5 +1897,16 @@ _drivecleaner:
|
|||
orderBySizeDesc: "ขนาดไฟล์จากมากไปหาน้อย"
|
||||
orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก"
|
||||
_webhookSettings:
|
||||
createWebhook: "สร้าง Webhook"
|
||||
name: "ชื่อ"
|
||||
secret: "ความลับ"
|
||||
events: "อีเว้นท์ Webhook"
|
||||
active: "เปิดใช้งาน"
|
||||
_events:
|
||||
follow: "เมื่อกำลังติดตามผู้ใช้"
|
||||
followed: "เมื่อกำลังติดตามแล้ว"
|
||||
note: "เมื่อกำลังโพสต์โน้ต"
|
||||
reply: "เมื่อได้รับการตอบกลับ"
|
||||
renote: "รีโน้ตแล้วเมื่อ"
|
||||
reaction: "เมื่อได้รับรีแอคชั่น"
|
||||
mention: "เมื่อกำลังถูกกล่าวถึง"
|
||||
|
|
|
@ -991,6 +991,7 @@ largeNoteReactions: "使用大图标来显示回应"
|
|||
noteIdOrUrl: "帖子ID或URL"
|
||||
accountMigration: "账户迁移"
|
||||
accountMoved: "此用户已迁移账户"
|
||||
forceShowAds: "总是显示广告"
|
||||
_accountMigration:
|
||||
moveTo: "把这个账户迁移到新的账户"
|
||||
moveToLabel: "迁移后的账户"
|
||||
|
|
|
@ -985,9 +985,15 @@ showClipButtonInNoteFooter: "將摘錄添加至貼文"
|
|||
largeNoteReactions: "將貼文的反應放大顯示"
|
||||
noteIdOrUrl: "貼文ID或URL"
|
||||
accountMigration: "遷移帳戶"
|
||||
forceShowAds: "總是顯示廣告"
|
||||
_accountMigration:
|
||||
moveTo: "將這個帳戶遷移至新的帳戶"
|
||||
moveToLabel: "要遷移的帳戶:"
|
||||
moveToLabel: "要遷移到的帳戶:"
|
||||
moveAccountDescription: "這個操作不可撤銷。首先,請確認已在要遷移到的帳戶中為這個帳戶建立了一個別名。建立別名之後,像這樣輸入你要遷移到的帳戶:@person@instance.com"
|
||||
moveFrom: "從其他帳戶遷移到這個帳戶"
|
||||
moveFromLabel: "要遷移過來的帳戶:"
|
||||
moveFromDescription: "如果你想把跟隨者從別的帳戶遷移過來,必須先在這裡建立別名。請務必在執行遷移之前建立別名!請像這樣輸入要遷移的帳戶:@person@instance.com"
|
||||
migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外,請確認在要遷移到的帳戶已經建立了一個別名。"
|
||||
_achievements:
|
||||
earnedAt: "獲得日期"
|
||||
_types:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "13.11.1",
|
||||
"version": "13.11.2",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
|
@ -37,8 +37,24 @@ const $redis: Provider = {
|
|||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $redisForPubsub: Provider = {
|
||||
provide: DI.redisForPubsub,
|
||||
const $redisForPub: Provider = {
|
||||
provide: DI.redisForPub,
|
||||
useFactory: (config) => {
|
||||
const redis = new Redis({
|
||||
port: config.redisForPubsub.port,
|
||||
host: config.redisForPubsub.host,
|
||||
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
|
||||
password: config.redisForPubsub.pass,
|
||||
keyPrefix: `${config.redisForPubsub.prefix}:`,
|
||||
db: config.redisForPubsub.db ?? 0,
|
||||
});
|
||||
return redis;
|
||||
},
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $redisForSub: Provider = {
|
||||
provide: DI.redisForSub,
|
||||
useFactory: (config) => {
|
||||
const redis = new Redis({
|
||||
port: config.redisForPubsub.port,
|
||||
|
@ -57,14 +73,15 @@ const $redisForPubsub: Provider = {
|
|||
@Global()
|
||||
@Module({
|
||||
imports: [RepositoryModule],
|
||||
providers: [$config, $db, $redis, $redisForPubsub],
|
||||
exports: [$config, $db, $redis, $redisForPubsub, RepositoryModule],
|
||||
providers: [$config, $db, $redis, $redisForPub, $redisForSub],
|
||||
exports: [$config, $db, $redis, $redisForPub, $redisForSub, RepositoryModule],
|
||||
})
|
||||
export class GlobalModule implements OnApplicationShutdown {
|
||||
constructor(
|
||||
@Inject(DI.db) private db: DataSource,
|
||||
@Inject(DI.redis) private redisClient: Redis.Redis,
|
||||
@Inject(DI.redisForPubsub) private redisForPubsub: Redis.Redis,
|
||||
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||
) {}
|
||||
|
||||
async onApplicationShutdown(signal: string): Promise<void> {
|
||||
|
@ -79,7 +96,8 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||
await Promise.all([
|
||||
this.db.destroy(),
|
||||
this.redisClient.disconnect(),
|
||||
this.redisForPubsub.disconnect(),
|
||||
this.redisForPub.disconnect(),
|
||||
this.redisForSub.disconnect(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,8 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
@ -52,12 +52,12 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
this.antennasFetched = false;
|
||||
this.antennas = [];
|
||||
|
||||
this.redisForPubsub.on('message', this.onRedisMessage);
|
||||
this.redisForSub.on('message', this.onRedisMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
this.redisForPubsub.off('message', this.onRedisMessage);
|
||||
this.redisForSub.off('message', this.onRedisMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -91,14 +91,24 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
|
||||
this.redisClient.xadd(
|
||||
`antennaTimeline:${antenna.id}`,
|
||||
'MAXLEN', '~', '200',
|
||||
`${this.idService.parse(note.id).date.getTime()}-*`,
|
||||
'note', note.id);
|
||||
|
||||
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
||||
public async addNoteToAntennas(note: Note, noteUser: { id: User['id']; username: string; host: string | null; }): Promise<void> {
|
||||
const antennas = await this.getAntennas();
|
||||
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
|
||||
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
|
||||
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
|
||||
for (const antenna of matchedAntennas) {
|
||||
redisPipeline.xadd(
|
||||
`antennaTimeline:${antenna.id}`,
|
||||
'MAXLEN', '~', '200',
|
||||
'*',
|
||||
'note', note.id);
|
||||
|
||||
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
||||
}
|
||||
|
||||
redisPipeline.exec();
|
||||
}
|
||||
|
||||
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||
|
|
|
@ -27,8 +27,8 @@ export class CacheService implements OnApplicationShutdown {
|
|||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
@ -116,7 +116,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
|
||||
this.redisForPubsub.on('message', this.onMessage);
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -167,6 +167,6 @@ export class CacheService implements OnApplicationShutdown {
|
|||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
this.redisForPubsub.off('message', this.onMessage);
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,12 +43,8 @@ export class CustomEmojiService {
|
|||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60 * 3, // 3m
|
||||
fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))),
|
||||
toRedisConverter: (value) => JSON.stringify(value.values()),
|
||||
fromRedisConverter: (value) => {
|
||||
// 原因不明だが配列以外が入ってくることがあるため
|
||||
if (!Array.isArray(JSON.parse(value))) return undefined;
|
||||
return new Map(JSON.parse(value).map((x: Emoji) => [x.name, x]));
|
||||
}, // TODO: Date型の変換
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value.values())),
|
||||
fromRedisConverter: (value) => new Map(JSON.parse(value).map((x: Emoji) => [x.name, x])), // TODO: Date型の変換
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -271,16 +267,7 @@ export class CustomEmojiService {
|
|||
const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
|
||||
|
||||
if (emoji == null) return null;
|
||||
|
||||
const isLocal = emoji.host == null;
|
||||
const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
const url = isLocal
|
||||
? emojiUrl
|
||||
: this.config.proxyRemoteFiles
|
||||
? `${this.config.mediaProxy}/emoji.webp?${query({ url: emojiUrl })}`
|
||||
: emojiUrl;
|
||||
|
||||
return url;
|
||||
return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -26,8 +26,8 @@ export class GlobalEventService {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.redisForPub)
|
||||
private redisForPub: Redis.Redis,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ export class GlobalEventService {
|
|||
{ type: type, body: null } :
|
||||
{ type: type, body: value };
|
||||
|
||||
this.redisClient.publish(this.config.host, JSON.stringify({
|
||||
this.redisForPub.publish(this.config.host, JSON.stringify({
|
||||
channel: channel,
|
||||
message: message,
|
||||
}));
|
||||
|
|
|
@ -14,8 +14,8 @@ export class MetaService implements OnApplicationShutdown {
|
|||
private intervalId: NodeJS.Timer;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
@ -33,7 +33,7 @@ export class MetaService implements OnApplicationShutdown {
|
|||
}, 1000 * 60 * 5);
|
||||
}
|
||||
|
||||
this.redisForPubsub.on('message', this.onMessage);
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -122,6 +122,6 @@ export class MetaService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
clearInterval(this.intervalId);
|
||||
this.redisForPubsub.off('message', this.onMessage);
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -329,7 +329,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
this.redisClient.xadd(
|
||||
`channelTimeline:${data.channel.id}`,
|
||||
'MAXLEN', '~', '1000',
|
||||
`${this.idService.parse(note.id).date.getTime()}-*`,
|
||||
'*',
|
||||
'note', note.id);
|
||||
}
|
||||
|
||||
|
@ -493,14 +493,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
});
|
||||
|
||||
// Antenna
|
||||
for (const antenna of (await this.antennaService.getAntennas())) {
|
||||
this.antennaService.checkHitAntenna(antenna, note, user).then(hit => {
|
||||
if (hit) {
|
||||
this.antennaService.addNoteToAntenna(antenna, note, user);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.antennaService.addNoteToAntennas(note, user);
|
||||
|
||||
if (data.reply) {
|
||||
this.saveReply(data.reply, note);
|
||||
|
|
|
@ -66,6 +66,7 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
private postReadAllNotifications(userId: User['id']) {
|
||||
this.globalEventService.publishMainStream(userId, 'readAllNotifications');
|
||||
this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -99,7 +100,7 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
const redisIdPromise = this.redisClient.xadd(
|
||||
`notificationTimeline:${notifieeId}`,
|
||||
'MAXLEN', '~', '300',
|
||||
`${this.idService.parse(notification.id).date.getTime()}-*`,
|
||||
'*',
|
||||
'data', JSON.stringify(notification));
|
||||
|
||||
const packed = await this.notificationEntityService.pack(notification, notifieeId, {});
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import push from 'web-push';
|
||||
import Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { Packed } from '@/misc/json-schema';
|
||||
import { getNoteSummary } from '@/misc/get-note-summary.js';
|
||||
import type { SwSubscriptionsRepository } from '@/models/index.js';
|
||||
import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RedisKVCache } from '@/misc/cache.js';
|
||||
|
||||
// Defined also packages/sw/types.ts#L13
|
||||
type PushNotificationsTypes = {
|
||||
|
@ -15,6 +17,7 @@ type PushNotificationsTypes = {
|
|||
antenna: { id: string, name: string };
|
||||
note: Packed<'Note'>;
|
||||
};
|
||||
'readAllNotifications': undefined;
|
||||
};
|
||||
|
||||
// Reduce length because push message servers have character limits
|
||||
|
@ -40,15 +43,27 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus
|
|||
|
||||
@Injectable()
|
||||
export class PushNotificationService {
|
||||
private subscriptionsCache: RedisKVCache<SwSubscription[]>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.swSubscriptionsRepository)
|
||||
private swSubscriptionsRepository: SwSubscriptionsRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
) {
|
||||
this.subscriptionsCache = new RedisKVCache<SwSubscription[]>(this.redisClient, 'userSwSubscriptions', {
|
||||
lifetime: 1000 * 60 * 60 * 1, // 1h
|
||||
memoryCacheLifetime: 1000 * 60 * 3, // 3m
|
||||
fetcher: (key) => this.swSubscriptionsRepository.findBy({ userId: key }),
|
||||
toRedisConverter: (value) => JSON.stringify(value),
|
||||
fromRedisConverter: (value) => JSON.parse(value),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -62,12 +77,13 @@ export class PushNotificationService {
|
|||
meta.swPublicKey,
|
||||
meta.swPrivateKey);
|
||||
|
||||
// Fetch
|
||||
const subscriptions = await this.swSubscriptionsRepository.findBy({
|
||||
userId: userId,
|
||||
});
|
||||
const subscriptions = await this.subscriptionsCache.fetch(userId);
|
||||
|
||||
for (const subscription of subscriptions) {
|
||||
if ([
|
||||
'readAllNotifications',
|
||||
].includes(type) && !subscription.sendReadMessage) continue;
|
||||
|
||||
const pushSubscription = {
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
|
|
|
@ -3,7 +3,7 @@ import Bull from 'bull';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData } from '../queue/types.js';
|
||||
import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData, DbJobMap } from '../queue/types.js';
|
||||
|
||||
function q<T>(config: Config, name: string, limitPerSec = -1) {
|
||||
return new Bull<T>(name, {
|
||||
|
@ -41,7 +41,8 @@ export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
|||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||
export type DeliverQueue = Bull.Queue<DeliverJobData>;
|
||||
export type InboxQueue = Bull.Queue<InboxJobData>;
|
||||
export type DbQueue = Bull.Queue<DbJobData>;
|
||||
export type DbQueue = Bull.Queue<DbJobData<keyof DbJobMap>>;
|
||||
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
|
||||
export type ObjectStorageQueue = Bull.Queue<ObjectStorageJobData>;
|
||||
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
|
||||
|
||||
|
@ -75,6 +76,12 @@ const $db: Provider = {
|
|||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $relationship: Provider = {
|
||||
provide: 'queue:relationship',
|
||||
useFactory: (config: Config) => q(config, 'relationship'),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $objectStorage: Provider = {
|
||||
provide: 'queue:objectStorage',
|
||||
useFactory: (config: Config) => q(config, 'objectStorage'),
|
||||
|
@ -96,6 +103,7 @@ const $webhookDeliver: Provider = {
|
|||
$deliver,
|
||||
$inbox,
|
||||
$db,
|
||||
$relationship,
|
||||
$objectStorage,
|
||||
$webhookDeliver,
|
||||
],
|
||||
|
@ -105,6 +113,7 @@ const $webhookDeliver: Provider = {
|
|||
$deliver,
|
||||
$inbox,
|
||||
$db,
|
||||
$relationship,
|
||||
$objectStorage,
|
||||
$webhookDeliver,
|
||||
],
|
||||
|
|
|
@ -6,9 +6,10 @@ import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
|
|||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
||||
import type { ThinUser } from '../queue/types.js';
|
||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
||||
import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
import Bull from 'bull';
|
||||
|
||||
@Injectable()
|
||||
export class QueueService {
|
||||
|
@ -21,6 +22,7 @@ export class QueueService {
|
|||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
||||
) {}
|
||||
|
@ -56,7 +58,7 @@ export class QueueService {
|
|||
activity: activity,
|
||||
signature,
|
||||
};
|
||||
|
||||
|
||||
return this.inboxQueue.add(data, {
|
||||
attempts: this.config.inboxJobMaxAttempts ?? 8,
|
||||
timeout: 5 * 60 * 1000, // 5min
|
||||
|
@ -71,7 +73,7 @@ export class QueueService {
|
|||
@bindThis
|
||||
public createDeleteDriveFilesJob(user: ThinUser) {
|
||||
return this.dbQueue.add('deleteDriveFiles', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
|
@ -81,7 +83,7 @@ export class QueueService {
|
|||
@bindThis
|
||||
public createExportCustomEmojisJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportCustomEmojis', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
|
@ -91,7 +93,7 @@ export class QueueService {
|
|||
@bindThis
|
||||
public createExportNotesJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportNotes', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
|
@ -101,7 +103,7 @@ export class QueueService {
|
|||
@bindThis
|
||||
public createExportFavoritesJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportFavorites', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
|
@ -111,7 +113,7 @@ export class QueueService {
|
|||
@bindThis
|
||||
public createExportFollowingJob(user: ThinUser, excludeMuting = false, excludeInactive = false) {
|
||||
return this.dbQueue.add('exportFollowing', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
excludeMuting,
|
||||
excludeInactive,
|
||||
}, {
|
||||
|
@ -123,7 +125,7 @@ export class QueueService {
|
|||
@bindThis
|
||||
public createExportMuteJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportMuting', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
|
@ -133,7 +135,7 @@ export class QueueService {
|
|||
@bindThis
|
||||
public createExportBlockingJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportBlocking', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
|
@ -143,7 +145,7 @@ export class QueueService {
|
|||
@bindThis
|
||||
public createExportUserListsJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportUserLists', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
|
@ -153,7 +155,7 @@ export class QueueService {
|
|||
@bindThis
|
||||
public createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) {
|
||||
return this.dbQueue.add('importFollowing', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
|
@ -161,10 +163,16 @@ export class QueueService {
|
|||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createImportFollowingToDbJob(user: ThinUser, targets: string[]) {
|
||||
const jobs = targets.map(rel => this.generateToDbJobData('importFollowingToDb', { user, target: rel }));
|
||||
return this.dbQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) {
|
||||
return this.dbQueue.add('importMuting', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
|
@ -175,7 +183,7 @@ export class QueueService {
|
|||
@bindThis
|
||||
public createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) {
|
||||
return this.dbQueue.add('importBlocking', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
|
@ -183,10 +191,32 @@ export class QueueService {
|
|||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createImportBlockingToDbJob(user: ThinUser, targets: string[]) {
|
||||
const jobs = targets.map(rel => this.generateToDbJobData('importBlockingToDb', { user, target: rel }));
|
||||
return this.dbQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private generateToDbJobData<T extends 'importFollowingToDb' | 'importBlockingToDb', D extends DbJobData<T>>(name: T, data: D): {
|
||||
name: string,
|
||||
data: D,
|
||||
opts: Bull.JobOptions,
|
||||
} {
|
||||
return {
|
||||
name,
|
||||
data,
|
||||
opts: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) {
|
||||
return this.dbQueue.add('importUserLists', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
|
@ -197,7 +227,7 @@ export class QueueService {
|
|||
@bindThis
|
||||
public createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) {
|
||||
return this.dbQueue.add('importCustomEmojis', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
|
@ -208,7 +238,7 @@ export class QueueService {
|
|||
@bindThis
|
||||
public createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) {
|
||||
return this.dbQueue.add('deleteAccount', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
soft: opts.soft,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
|
@ -216,6 +246,51 @@ export class QueueService {
|
|||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean }[]) {
|
||||
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
|
||||
return this.relationshipQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createUnfollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string }[]) {
|
||||
const jobs = followings.map(rel => this.generateRelationshipJobData('unfollow', rel));
|
||||
return this.relationshipQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createBlockJob(blockings: { from: ThinUser, to: ThinUser, silent?: boolean }[]) {
|
||||
const jobs = blockings.map(rel => this.generateRelationshipJobData('block', rel));
|
||||
return this.relationshipQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createUnblockJob(blockings: { from: ThinUser, to: ThinUser, silent?: boolean }[]) {
|
||||
const jobs = blockings.map(rel => this.generateRelationshipJobData('unblock', rel));
|
||||
return this.relationshipQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData): {
|
||||
name: string,
|
||||
data: RelationshipJobData,
|
||||
opts: Bull.JobOptions,
|
||||
} {
|
||||
return {
|
||||
name,
|
||||
data: {
|
||||
from: { id: data.from.id },
|
||||
to: { id: data.to.id },
|
||||
silent: data.silent,
|
||||
requestId: data.requestId,
|
||||
},
|
||||
opts: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createDeleteObjectStorageFileJob(key: string) {
|
||||
return this.objectStorageQueue.add('deleteFile', {
|
||||
|
@ -246,7 +321,7 @@ export class QueueService {
|
|||
createdAt: Date.now(),
|
||||
eventId: uuid(),
|
||||
};
|
||||
|
||||
|
||||
return this.webhookDeliverQueue.add(data, {
|
||||
attempts: 4,
|
||||
timeout: 1 * 60 * 1000, // 1min
|
||||
|
@ -264,7 +339,7 @@ export class QueueService {
|
|||
//deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
});
|
||||
this.deliverQueue.clean(0, 'delayed');
|
||||
|
||||
|
||||
this.inboxQueue.once('cleaned', (jobs, status) => {
|
||||
//inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
});
|
||||
|
|
|
@ -64,8 +64,8 @@ export class RoleService implements OnApplicationShutdown {
|
|||
public static NotAssignedError = class extends Error {};
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
@ -87,7 +87,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||
this.rolesCache = new MemorySingleCache<Role[]>(1000 * 60 * 60 * 1);
|
||||
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(1000 * 60 * 60 * 1);
|
||||
|
||||
this.redisForPubsub.on('message', this.onMessage);
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -400,6 +400,6 @@ export class RoleService implements OnApplicationShutdown {
|
|||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
this.redisForPubsub.off('message', this.onMessage);
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ export class UserBlockingService implements OnModuleInit {
|
|||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
|
@ -54,12 +54,12 @@ export class UserBlockingService implements OnModuleInit {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async block(blocker: User, blockee: User) {
|
||||
public async block(blocker: User, blockee: User, silent = false) {
|
||||
await Promise.all([
|
||||
this.cancelRequest(blocker, blockee),
|
||||
this.cancelRequest(blockee, blocker),
|
||||
this.userFollowingService.unfollow(blocker, blockee),
|
||||
this.userFollowingService.unfollow(blockee, blocker),
|
||||
this.cancelRequest(blocker, blockee, silent),
|
||||
this.cancelRequest(blockee, blocker, silent),
|
||||
this.userFollowingService.unfollow(blocker, blockee, silent),
|
||||
this.userFollowingService.unfollow(blockee, blocker, silent),
|
||||
this.removeFromList(blockee, blocker),
|
||||
]);
|
||||
|
||||
|
@ -89,7 +89,7 @@ export class UserBlockingService implements OnModuleInit {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async cancelRequest(follower: User, followee: User) {
|
||||
private async cancelRequest(follower: User, followee: User, silent = false) {
|
||||
const request = await this.followRequestsRepository.findOneBy({
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
|
@ -110,7 +110,7 @@ export class UserBlockingService implements OnModuleInit {
|
|||
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(follower)) {
|
||||
if (this.userEntityService.isLocalUser(follower) && !silent) {
|
||||
this.userEntityService.pack(followee, follower, {
|
||||
detail: true,
|
||||
}).then(async packed => {
|
||||
|
|
|
@ -43,7 +43,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
|
@ -79,7 +79,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
|
||||
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string, silent = false): Promise<void> {
|
||||
const [follower, followee] = await Promise.all([
|
||||
this.usersRepository.findOneByOrFail({ id: _follower.id }),
|
||||
this.usersRepository.findOneByOrFail({ id: _followee.id }),
|
||||
|
@ -139,7 +139,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
}
|
||||
}
|
||||
|
||||
await this.insertFollowingDoc(followee, follower);
|
||||
await this.insertFollowingDoc(followee, follower, silent);
|
||||
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||||
|
@ -155,6 +155,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
follower: {
|
||||
id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']
|
||||
},
|
||||
silent = false,
|
||||
): Promise<void> {
|
||||
if (follower.id === followee.id) return;
|
||||
|
||||
|
@ -233,7 +234,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
this.perUserFollowingChart.update(follower, followee, true);
|
||||
|
||||
// Publish follow event
|
||||
if (this.userEntityService.isLocalUser(follower)) {
|
||||
if (this.userEntityService.isLocalUser(follower) && !silent) {
|
||||
this.userEntityService.pack(followee.id, follower, {
|
||||
detail: true,
|
||||
}).then(async packed => {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserListService {
|
||||
|
@ -29,6 +30,7 @@ export class UserListService {
|
|||
private roleService: RoleService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private proxyAccountService: ProxyAccountService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -47,14 +49,14 @@ export class UserListService {
|
|||
userId: target.id,
|
||||
userListId: list.id,
|
||||
} as UserListJoining);
|
||||
|
||||
|
||||
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
|
||||
|
||||
|
||||
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
|
||||
if (this.userEntityService.isRemoteUser(target)) {
|
||||
const proxy = await this.proxyAccountService.fetch();
|
||||
if (proxy) {
|
||||
this.userFollowingService.follow(proxy, target);
|
||||
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: target.id } }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,14 +13,14 @@ export class WebhookService implements OnApplicationShutdown {
|
|||
private webhooks: Webhook[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.webhooksRepository)
|
||||
private webhooksRepository: WebhooksRepository,
|
||||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
this.redisForPubsub.on('message', this.onMessage);
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -82,6 +82,6 @@ export class WebhookService implements OnApplicationShutdown {
|
|||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
this.redisForPubsub.off('message', this.onMessage);
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@ export const DI = {
|
|||
config: Symbol('config'),
|
||||
db: Symbol('db'),
|
||||
redis: Symbol('redis'),
|
||||
redisForPubsub: Symbol('redisForPubsub'),
|
||||
redisForPub: Symbol('redisForPub'),
|
||||
redisForSub: Symbol('redisForSub'),
|
||||
|
||||
//#region Repositories
|
||||
usersRepository: Symbol('usersRepository'),
|
||||
|
|
|
@ -8,7 +8,7 @@ export class RedisKVCache<T> {
|
|||
private memoryCache: MemoryKVCache<T>;
|
||||
private fetcher: (key: string) => Promise<T>;
|
||||
private toRedisConverter: (value: T) => string;
|
||||
private fromRedisConverter: (value: string) => T | undefined; // undefined means no cache
|
||||
private fromRedisConverter: (value: string) => T;
|
||||
|
||||
constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: {
|
||||
lifetime: RedisKVCache<T>['lifetime'];
|
||||
|
@ -92,7 +92,7 @@ export class RedisSingleCache<T> {
|
|||
private memoryCache: MemorySingleCache<T>;
|
||||
private fetcher: () => Promise<T>;
|
||||
private toRedisConverter: (value: T) => string;
|
||||
private fromRedisConverter: (value: string) => T | undefined; // undefined means no cache
|
||||
private fromRedisConverter: (value: string) => T;
|
||||
|
||||
constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: {
|
||||
lifetime: RedisSingleCache<T>['lifetime'];
|
||||
|
|
|
@ -52,8 +52,10 @@ export class DbQueueProcessorsService {
|
|||
q.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done));
|
||||
q.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done));
|
||||
q.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done));
|
||||
q.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job));
|
||||
q.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done));
|
||||
q.process('importBlocking', (job, done) => this.importBlockingProcessorService.process(job, done));
|
||||
q.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job));
|
||||
q.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done));
|
||||
q.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done));
|
||||
q.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job));
|
||||
|
|
|
@ -4,6 +4,7 @@ import { GlobalModule } from '@/GlobalModule.js';
|
|||
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||
import { QueueProcessorService } from './QueueProcessorService.js';
|
||||
import { DbQueueProcessorsService } from './DbQueueProcessorsService.js';
|
||||
import { RelationshipQueueProcessorsService } from './RelationshipQueueProcessorsService.js';
|
||||
import { ObjectStorageQueueProcessorsService } from './ObjectStorageQueueProcessorsService.js';
|
||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||
|
@ -32,6 +33,7 @@ import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessor
|
|||
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
|
||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
||||
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -61,9 +63,11 @@ import { ExportFavoritesProcessorService } from './processors/ExportFavoritesPro
|
|||
DeleteAccountProcessorService,
|
||||
DeleteFileProcessorService,
|
||||
CleanRemoteFilesProcessorService,
|
||||
RelationshipProcessorService,
|
||||
SystemQueueProcessorsService,
|
||||
ObjectStorageQueueProcessorsService,
|
||||
DbQueueProcessorsService,
|
||||
RelationshipQueueProcessorsService,
|
||||
WebhookDeliverProcessorService,
|
||||
EndedPollNotificationProcessorService,
|
||||
DeliverProcessorService,
|
||||
|
|
|
@ -13,6 +13,7 @@ import { EndedPollNotificationProcessorService } from './processors/EndedPollNot
|
|||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||
import { RelationshipQueueProcessorsService } from './RelationshipQueueProcessorsService.js';
|
||||
|
||||
@Injectable()
|
||||
export class QueueProcessorService {
|
||||
|
@ -27,6 +28,7 @@ export class QueueProcessorService {
|
|||
private systemQueueProcessorsService: SystemQueueProcessorsService,
|
||||
private objectStorageQueueProcessorsService: ObjectStorageQueueProcessorsService,
|
||||
private dbQueueProcessorsService: DbQueueProcessorsService,
|
||||
private relationshipQueueProcessorsService: RelationshipQueueProcessorsService,
|
||||
private webhookDeliverProcessorService: WebhookDeliverProcessorService,
|
||||
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
||||
private deliverProcessorService: DeliverProcessorService,
|
||||
|
@ -52,14 +54,15 @@ export class QueueProcessorService {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const systemLogger = this.logger.createSubLogger('system');
|
||||
const deliverLogger = this.logger.createSubLogger('deliver');
|
||||
const webhookLogger = this.logger.createSubLogger('webhook');
|
||||
const inboxLogger = this.logger.createSubLogger('inbox');
|
||||
const dbLogger = this.logger.createSubLogger('db');
|
||||
const relationshipLogger = this.logger.createSubLogger('relationship');
|
||||
const objectStorageLogger = this.logger.createSubLogger('objectStorage');
|
||||
|
||||
|
||||
this.queueService.systemQueue
|
||||
.on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
|
||||
|
@ -67,7 +70,7 @@ export class QueueProcessorService {
|
|||
.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`));
|
||||
|
||||
|
||||
this.queueService.deliverQueue
|
||||
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
|
@ -75,7 +78,7 @@ export class QueueProcessorService {
|
|||
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
|
||||
.on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
||||
|
||||
|
||||
this.queueService.inboxQueue
|
||||
.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
|
||||
|
@ -83,7 +86,7 @@ export class QueueProcessorService {
|
|||
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`));
|
||||
|
||||
|
||||
this.queueService.dbQueue
|
||||
.on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
|
||||
|
@ -91,7 +94,15 @@ export class QueueProcessorService {
|
|||
.on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`));
|
||||
|
||||
|
||||
this.queueService.relationshipQueue
|
||||
.on('waiting', (jobId) => relationshipLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => relationshipLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => relationshipLogger.warn(`stalled id=${job.id}`));
|
||||
|
||||
this.queueService.objectStorageQueue
|
||||
.on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
|
||||
|
@ -99,7 +110,7 @@ export class QueueProcessorService {
|
|||
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
|
||||
|
||||
|
||||
this.queueService.webhookDeliverQueue
|
||||
.on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
|
@ -107,26 +118,27 @@ export class QueueProcessorService {
|
|||
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
|
||||
.on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
||||
|
||||
|
||||
this.queueService.deliverQueue.process(this.config.deliverJobConcurrency ?? 128, (job) => this.deliverProcessorService.process(job));
|
||||
this.queueService.inboxQueue.process(this.config.inboxJobConcurrency ?? 16, (job) => this.inboxProcessorService.process(job));
|
||||
this.queueService.endedPollNotificationQueue.process((job, done) => this.endedPollNotificationProcessorService.process(job, done));
|
||||
this.queueService.webhookDeliverQueue.process(64, (job) => this.webhookDeliverProcessorService.process(job));
|
||||
this.dbQueueProcessorsService.start(this.queueService.dbQueue);
|
||||
this.relationshipQueueProcessorsService.start(this.queueService.relationshipQueue);
|
||||
this.objectStorageQueueProcessorsService.start(this.queueService.objectStorageQueue);
|
||||
|
||||
|
||||
this.queueService.systemQueue.add('tickCharts', {
|
||||
}, {
|
||||
repeat: { cron: '55 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
|
||||
this.queueService.systemQueue.add('resyncCharts', {
|
||||
}, {
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
|
||||
this.queueService.systemQueue.add('cleanCharts', {
|
||||
}, {
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
|
@ -138,19 +150,19 @@ export class QueueProcessorService {
|
|||
repeat: { cron: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
|
||||
this.queueService.systemQueue.add('clean', {
|
||||
}, {
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
|
||||
this.queueService.systemQueue.add('checkExpiredMutings', {
|
||||
}, {
|
||||
repeat: { cron: '*/5 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
|
||||
this.systemQueueProcessorsService.start(this.queueService.systemQueue);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
|
||||
import type Bull from 'bull';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
||||
@Injectable()
|
||||
export class RelationshipQueueProcessorsService {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
private relationshipProcessorService: RelationshipProcessorService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public start(q: Bull.Queue): void {
|
||||
const maxJobs = (this.config.deliverJobConcurrency ?? 128) / 4; // conservative?
|
||||
q.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job));
|
||||
q.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job));
|
||||
q.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job));
|
||||
q.process('unblock', maxJobs, (job) => this.relationshipProcessorService.processUnblock(job));
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import type Logger from '@/logger.js';
|
|||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbUserJobData } from '../types.js';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -31,7 +31,7 @@ export class DeleteDriveFilesProcessorService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
||||
this.logger.info(`Deleting drive files of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
|
|
|
@ -11,7 +11,7 @@ import { createTemp } from '@/misc/create-temp.js';
|
|||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbUserJobData } from '../types.js';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -36,7 +36,7 @@ export class ExportBlockingProcessorService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
||||
this.logger.info(`Exporting blocking of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
|
|
|
@ -13,7 +13,7 @@ import type { Note } from '@/models/entities/Note.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbUserJobData } from '../types.js';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ExportFavoritesProcessorService {
|
||||
|
@ -42,7 +42,7 @@ export class ExportFavoritesProcessorService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
||||
this.logger.info(`Exporting favorites of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
|
|
|
@ -12,7 +12,7 @@ import type { Following } from '@/models/entities/Following.js';
|
|||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbUserJobData } from '../types.js';
|
||||
import type { DbExportFollowingData } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -40,7 +40,7 @@ export class ExportFollowingProcessorService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbExportFollowingData>, done: () => void): Promise<void> {
|
||||
this.logger.info(`Exporting following of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
|
|
|
@ -11,7 +11,7 @@ import { createTemp } from '@/misc/create-temp.js';
|
|||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbUserJobData } from '../types.js';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -39,7 +39,7 @@ export class ExportMutingProcessorService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
||||
this.logger.info(`Exporting muting of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
|
|
|
@ -13,7 +13,7 @@ import type { Note } from '@/models/entities/Note.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbUserJobData } from '../types.js';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ExportNotesProcessorService {
|
||||
|
@ -39,7 +39,7 @@ export class ExportNotesProcessorService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
||||
this.logger.info(`Exporting notes of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
|
|
|
@ -11,7 +11,7 @@ import { createTemp } from '@/misc/create-temp.js';
|
|||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbUserJobData } from '../types.js';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -39,7 +39,7 @@ export class ExportUserListsProcessorService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
||||
this.logger.info(`Exporting user lists of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
|
|
|
@ -1,38 +1,31 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository, BlockingsRepository, DriveFilesRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { UsersRepository, DriveFilesRepository } from '@/models/index.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbUserImportJobData } from '../types.js';
|
||||
import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
@Injectable()
|
||||
export class ImportBlockingProcessorService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private queueService: QueueService,
|
||||
private utilityService: UtilityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
private remoteUserResolveService: RemoteUserResolveService,
|
||||
private downloadService: DownloadService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
|
@ -59,46 +52,50 @@ export class ImportBlockingProcessorService {
|
|||
}
|
||||
|
||||
const csv = await this.downloadService.downloadTextFile(file.url);
|
||||
const targets = csv.trim().split('\n');
|
||||
this.queueService.createImportBlockingToDbJob({ id: user.id }, targets);
|
||||
|
||||
let linenum = 0;
|
||||
|
||||
for (const line of csv.trim().split('\n')) {
|
||||
linenum++;
|
||||
|
||||
try {
|
||||
const acct = line.split(',')[0].trim();
|
||||
const { username, host } = Acct.parse(acct);
|
||||
|
||||
let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({
|
||||
host: IsNull(),
|
||||
usernameLower: username.toLowerCase(),
|
||||
}) : await this.usersRepository.findOneBy({
|
||||
host: this.utilityService.toPuny(host!),
|
||||
usernameLower: username.toLowerCase(),
|
||||
});
|
||||
|
||||
if (host == null && target == null) continue;
|
||||
|
||||
if (target == null) {
|
||||
target = await this.remoteUserResolveService.resolveUser(username, host);
|
||||
}
|
||||
|
||||
if (target == null) {
|
||||
throw `cannot resolve user: @${username}@${host}`;
|
||||
}
|
||||
|
||||
// skip myself
|
||||
if (target.id === job.data.user.id) continue;
|
||||
|
||||
this.logger.info(`Block[${linenum}] ${target.id} ...`);
|
||||
|
||||
await this.userBlockingService.block(user, target);
|
||||
} catch (e) {
|
||||
this.logger.warn(`Error in line:${linenum} ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.succ('Imported');
|
||||
this.logger.succ('Import jobs created');
|
||||
done();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async processDb(job: Bull.Job<DbUserImportToDbJobData>): Promise<void> {
|
||||
const line = job.data.target;
|
||||
const user = job.data.user;
|
||||
|
||||
try {
|
||||
const acct = line.split(',')[0].trim();
|
||||
const { username, host } = Acct.parse(acct);
|
||||
|
||||
if (!host) return;
|
||||
|
||||
let target = this.utilityService.isSelfHost(host) ? await this.usersRepository.findOneBy({
|
||||
host: IsNull(),
|
||||
usernameLower: username.toLowerCase(),
|
||||
}) : await this.usersRepository.findOneBy({
|
||||
host: this.utilityService.toPuny(host),
|
||||
usernameLower: username.toLowerCase(),
|
||||
});
|
||||
|
||||
if (host == null && target == null) return;
|
||||
|
||||
if (target == null) {
|
||||
target = await this.remoteUserResolveService.resolveUser(username, host);
|
||||
}
|
||||
|
||||
if (target == null) {
|
||||
throw `Unable to resolve user: @${username}@${host}`;
|
||||
}
|
||||
|
||||
// skip myself
|
||||
if (target.id === job.data.user.id) return;
|
||||
|
||||
this.logger.info(`Block ${target.id} ...`);
|
||||
|
||||
this.queueService.createBlockJob([{ from: { id: user.id }, to: { id: target.id }, silent: true }]);
|
||||
} catch (e) {
|
||||
this.logger.warn(`Error: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,34 +2,30 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository, DriveFilesRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbUserImportJobData } from '../types.js';
|
||||
import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
@Injectable()
|
||||
export class ImportFollowingProcessorService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private queueService: QueueService,
|
||||
private utilityService: UtilityService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private remoteUserResolveService: RemoteUserResolveService,
|
||||
private downloadService: DownloadService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
|
@ -56,46 +52,50 @@ export class ImportFollowingProcessorService {
|
|||
}
|
||||
|
||||
const csv = await this.downloadService.downloadTextFile(file.url);
|
||||
const targets = csv.trim().split('\n');
|
||||
this.queueService.createImportFollowingToDbJob({ id: user.id }, targets);
|
||||
|
||||
let linenum = 0;
|
||||
|
||||
for (const line of csv.trim().split('\n')) {
|
||||
linenum++;
|
||||
|
||||
try {
|
||||
const acct = line.split(',')[0].trim();
|
||||
const { username, host } = Acct.parse(acct);
|
||||
|
||||
let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({
|
||||
host: IsNull(),
|
||||
usernameLower: username.toLowerCase(),
|
||||
}) : await this.usersRepository.findOneBy({
|
||||
host: this.utilityService.toPuny(host!),
|
||||
usernameLower: username.toLowerCase(),
|
||||
});
|
||||
|
||||
if (host == null && target == null) continue;
|
||||
|
||||
if (target == null) {
|
||||
target = await this.remoteUserResolveService.resolveUser(username, host);
|
||||
}
|
||||
|
||||
if (target == null) {
|
||||
throw `cannot resolve user: @${username}@${host}`;
|
||||
}
|
||||
|
||||
// skip myself
|
||||
if (target.id === job.data.user.id) continue;
|
||||
|
||||
this.logger.info(`Follow[${linenum}] ${target.id} ...`);
|
||||
|
||||
this.userFollowingService.follow(user, target);
|
||||
} catch (e) {
|
||||
this.logger.warn(`Error in line:${linenum} ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.succ('Imported');
|
||||
this.logger.succ('Import jobs created');
|
||||
done();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async processDb(job: Bull.Job<DbUserImportToDbJobData>): Promise<void> {
|
||||
const line = job.data.target;
|
||||
const user = job.data.user;
|
||||
|
||||
try {
|
||||
const acct = line.split(',')[0].trim();
|
||||
const { username, host } = Acct.parse(acct);
|
||||
|
||||
if (!host) return;
|
||||
|
||||
let target = this.utilityService.isSelfHost(host) ? await this.usersRepository.findOneBy({
|
||||
host: IsNull(),
|
||||
usernameLower: username.toLowerCase(),
|
||||
}) : await this.usersRepository.findOneBy({
|
||||
host: this.utilityService.toPuny(host),
|
||||
usernameLower: username.toLowerCase(),
|
||||
});
|
||||
|
||||
if (host == null && target == null) return;
|
||||
|
||||
if (target == null) {
|
||||
target = await this.remoteUserResolveService.resolveUser(username, host);
|
||||
}
|
||||
|
||||
if (target == null) {
|
||||
throw `Unable to resolve user: @${username}@${host}`;
|
||||
}
|
||||
|
||||
// skip myself
|
||||
if (target.id === job.data.user.id) return;
|
||||
|
||||
this.logger.info(`Follow ${target.id} ...`);
|
||||
|
||||
this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true }]);
|
||||
} catch (e) {
|
||||
this.logger.warn(`Error: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,11 +66,13 @@ export class ImportMutingProcessorService {
|
|||
const acct = line.split(',')[0].trim();
|
||||
const { username, host } = Acct.parse(acct);
|
||||
|
||||
let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({
|
||||
if (!host) continue;
|
||||
|
||||
let target = this.utilityService.isSelfHost(host) ? await this.usersRepository.findOneBy({
|
||||
host: IsNull(),
|
||||
usernameLower: username.toLowerCase(),
|
||||
}) : await this.usersRepository.findOneBy({
|
||||
host: this.utilityService.toPuny(host!),
|
||||
host: this.utilityService.toPuny(host),
|
||||
usernameLower: username.toLowerCase(),
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type Bull from 'bull';
|
||||
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type Logger from '@/logger.js';
|
||||
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import { RelationshipJobData } from '../types.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
@Injectable()
|
||||
export class RelationshipProcessorService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('follow-block');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async processFollow(job: Bull.Job<RelationshipJobData>): Promise<string> {
|
||||
this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id}`);
|
||||
await this.userFollowingService.follow(job.data.from, job.data.to, job.data.requestId, job.data.silent);
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async processUnfollow(job: Bull.Job<RelationshipJobData>): Promise<string> {
|
||||
this.logger.info(`${job.data.from.id} is trying to unfollow ${job.data.to.id}`);
|
||||
const [follower, followee] = await Promise.all([
|
||||
this.usersRepository.findOneByOrFail({ id: job.data.from.id }),
|
||||
this.usersRepository.findOneByOrFail({ id: job.data.to.id }),
|
||||
]);
|
||||
await this.userFollowingService.unfollow(follower, followee, job.data.silent);
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async processBlock(job: Bull.Job<RelationshipJobData>): Promise<string> {
|
||||
this.logger.info(`${job.data.from.id} is trying to block ${job.data.to.id}`);
|
||||
const [blockee, blocker] = await Promise.all([
|
||||
this.usersRepository.findOneByOrFail({ id: job.data.from.id }),
|
||||
this.usersRepository.findOneByOrFail({ id: job.data.to.id }),
|
||||
]);
|
||||
await this.userBlockingService.block(blockee, blocker, job.data.silent);
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async processUnblock(job: Bull.Job<RelationshipJobData>): Promise<string> {
|
||||
this.logger.info(`${job.data.from.id} is trying to unblock ${job.data.to.id}`);
|
||||
const [blockee, blocker] = await Promise.all([
|
||||
this.usersRepository.findOneByOrFail({ id: job.data.from.id }),
|
||||
this.usersRepository.findOneByOrFail({ id: job.data.to.id }),
|
||||
]);
|
||||
await this.userBlockingService.unblock(blockee, blocker);
|
||||
return 'ok';
|
||||
}
|
||||
}
|
|
@ -21,9 +21,39 @@ export type InboxJobData = {
|
|||
signature: httpSignature.IParsedSignature;
|
||||
};
|
||||
|
||||
export type DbJobData = DbUserJobData | DbUserImportJobData | DbUserDeleteJobData;
|
||||
export type RelationshipJobData = {
|
||||
from: ThinUser;
|
||||
to: ThinUser;
|
||||
silent?: boolean;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
export type DbUserJobData = {
|
||||
export type DbJobData<T extends keyof DbJobMap> = DbJobMap[T];
|
||||
|
||||
export type DbJobMap = {
|
||||
deleteDriveFiles: DbJobDataWithUser;
|
||||
exportCustomEmojis: DbJobDataWithUser;
|
||||
exportNotes: DbJobDataWithUser;
|
||||
exportFavorites: DbJobDataWithUser;
|
||||
exportFollowing: DbExportFollowingData;
|
||||
exportMuting: DbJobDataWithUser;
|
||||
exportBlocking: DbJobDataWithUser;
|
||||
exportUserLists: DbJobDataWithUser;
|
||||
importFollowing: DbUserImportJobData;
|
||||
importFollowingToDb: DbUserImportToDbJobData;
|
||||
importMuting: DbUserImportJobData;
|
||||
importBlocking: DbUserImportJobData;
|
||||
importBlockingToDb: DbUserImportToDbJobData;
|
||||
importUserLists: DbUserImportJobData;
|
||||
importCustomEmojis: DbUserImportJobData;
|
||||
deleteAccount: DbUserDeleteJobData;
|
||||
}
|
||||
|
||||
export type DbJobDataWithUser = {
|
||||
user: ThinUser;
|
||||
}
|
||||
|
||||
export type DbExportFollowingData = {
|
||||
user: ThinUser;
|
||||
excludeMuting: boolean;
|
||||
excludeInactive: boolean;
|
||||
|
@ -39,6 +69,11 @@ export type DbUserImportJobData = {
|
|||
fileId: DriveFile['id'];
|
||||
};
|
||||
|
||||
export type DbUserImportToDbJobData = {
|
||||
user: ThinUser;
|
||||
target: string;
|
||||
};
|
||||
|
||||
export type ObjectStorageJobData = ObjectStorageFileJobData | Record<string, unknown>;
|
||||
|
||||
export type ObjectStorageFileJobData = {
|
||||
|
|
|
@ -89,7 +89,7 @@ export class ApiServerService {
|
|||
Params: { endpoint: string; },
|
||||
Body: Record<string, unknown>,
|
||||
Querystring: Record<string, unknown>,
|
||||
}>('/' + endpoint.name, { bodyLimit: 1024 * 32 }, async (request, reply) => {
|
||||
}>('/' + endpoint.name, { bodyLimit: 1024 * 1024 }, async (request, reply) => {
|
||||
if (request.method === 'GET' && !endpoint.meta.allowGet) {
|
||||
reply.code(405);
|
||||
reply.send();
|
||||
|
|
|
@ -98,6 +98,7 @@ import * as ep___channels_update from './endpoints/channels/update.js';
|
|||
import * as ep___channels_favorite from './endpoints/channels/favorite.js';
|
||||
import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
|
||||
import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
|
||||
import * as ep___channels_search from './endpoints/channels/search.js';
|
||||
import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
|
||||
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
||||
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
||||
|
@ -431,6 +432,7 @@ const $channels_update: Provider = { provide: 'ep:channels/update', useClass: ep
|
|||
const $channels_favorite: Provider = { provide: 'ep:channels/favorite', useClass: ep___channels_favorite.default };
|
||||
const $channels_unfavorite: Provider = { provide: 'ep:channels/unfavorite', useClass: ep___channels_unfavorite.default };
|
||||
const $channels_myFavorites: Provider = { provide: 'ep:channels/my-favorites', useClass: ep___channels_myFavorites.default };
|
||||
const $channels_search: Provider = { provide: 'ep:channels/search', useClass: ep___channels_search.default };
|
||||
const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useClass: ep___charts_activeUsers.default };
|
||||
const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default };
|
||||
const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
|
||||
|
@ -768,6 +770,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$channels_favorite,
|
||||
$channels_unfavorite,
|
||||
$channels_myFavorites,
|
||||
$channels_search,
|
||||
$charts_activeUsers,
|
||||
$charts_apRequest,
|
||||
$charts_drive,
|
||||
|
@ -1099,6 +1102,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$channels_favorite,
|
||||
$channels_unfavorite,
|
||||
$channels_myFavorites,
|
||||
$channels_search,
|
||||
$charts_activeUsers,
|
||||
$charts_apRequest,
|
||||
$charts_drive,
|
||||
|
|
|
@ -22,8 +22,8 @@ export class StreamingApiServerService {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
@ -81,7 +81,7 @@ export class StreamingApiServerService {
|
|||
ev.emit(parsed.channel, parsed.message);
|
||||
}
|
||||
|
||||
this.redisForPubsub.on('message', onRedisMessage);
|
||||
this.redisForSub.on('message', onRedisMessage);
|
||||
|
||||
const main = new MainStreamConnection(
|
||||
this.channelsService,
|
||||
|
@ -111,7 +111,7 @@ export class StreamingApiServerService {
|
|||
connection.once('close', () => {
|
||||
ev.removeAllListeners();
|
||||
main.dispose();
|
||||
this.redisForPubsub.off('message', onRedisMessage);
|
||||
this.redisForSub.off('message', onRedisMessage);
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
});
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@ import * as ep___channels_update from './endpoints/channels/update.js';
|
|||
import * as ep___channels_favorite from './endpoints/channels/favorite.js';
|
||||
import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
|
||||
import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
|
||||
import * as ep___channels_search from './endpoints/channels/search.js';
|
||||
import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
|
||||
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
||||
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
||||
|
@ -429,6 +430,7 @@ const eps = [
|
|||
['channels/favorite', ep___channels_favorite],
|
||||
['channels/unfavorite', ep___channels_unfavorite],
|
||||
['channels/my-favorites', ep___channels_myFavorites],
|
||||
['channels/search', ep___channels_search],
|
||||
['charts/active-users', ep___charts_activeUsers],
|
||||
['charts/ap-request', ep___charts_apRequest],
|
||||
['charts/drive', ep___charts_drive],
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { FollowingsRepository, UsersRepository } from '@/models/index.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
@ -29,7 +29,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
@Inject(DI.notesRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private userFollowingService: UserFollowingService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const followings = await this.followingsRepository.findBy({
|
||||
|
@ -41,9 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
this.usersRepository.findOneByOrFail({ id: f.followeeId }),
|
||||
])));
|
||||
|
||||
for (const pair of pairs) {
|
||||
this.userFollowingService.unfollow(pair[0], pair[1]);
|
||||
}
|
||||
this.queueService.createUnfollowJob(pairs.map(p => ({ to: p[0], from: p[1], silent: true })));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { IsNull, Not } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository, FollowingsRepository } from '@/models/index.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { RelationshipJobData } from '@/queue/types.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { UserSuspendService } from '@/core/UserSuspendService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
@ -36,12 +36,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private userSuspendService: UserSuspendService,
|
||||
private roleService: RoleService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
|
@ -71,20 +69,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
|
||||
@bindThis
|
||||
private async unFollowAll(follower: User) {
|
||||
const followings = await this.followingsRepository.findBy({
|
||||
followerId: follower.id,
|
||||
const followings = await this.followingsRepository.find({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const jobs: RelationshipJobData[] = [];
|
||||
for (const following of followings) {
|
||||
const followee = await this.usersRepository.findOneBy({
|
||||
id: following.followeeId,
|
||||
});
|
||||
|
||||
if (followee == null) {
|
||||
throw `Cant find followee ${following.followeeId}`;
|
||||
if (following.followeeId && following.followerId) {
|
||||
jobs.push({
|
||||
from: { id: following.followerId },
|
||||
to: { id: following.followeeId },
|
||||
silent: true,
|
||||
});
|
||||
}
|
||||
|
||||
await this.userFollowingService.unfollow(follower, followee, true);
|
||||
}
|
||||
this.queueService.createUnfollowJob(jobs);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import type { ChannelsRepository } from '@/models/index.js';
|
||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['channels'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Channel',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string' },
|
||||
type: { type: 'string', enum: ['nameAndDescription', 'nameOnly'], default: 'nameAndDescription' },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 5 },
|
||||
},
|
||||
required: ['query'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.channelsRepository)
|
||||
private channelsRepository: ChannelsRepository,
|
||||
|
||||
private channelEntityService: ChannelEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId);
|
||||
|
||||
if (ps.type === 'nameAndDescription') {
|
||||
query.andWhere(new Brackets(qb => { qb
|
||||
.where('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` })
|
||||
.orWhere('channel.description ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
|
||||
}));
|
||||
} else {
|
||||
query.andWhere('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
|
||||
}
|
||||
|
||||
const channels = await query
|
||||
.take(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me)));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -54,10 +54,10 @@ class LocalTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.user!.showTimelineReplies) {
|
||||
if (note.reply && this.user && !this.user.showTimelineReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
|
|
|
@ -77,6 +77,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
|
|||
createdAt: '2016-12-28T22:49:51.000Z',
|
||||
description: 'I am a cool user!',
|
||||
ffVisibility: 'public',
|
||||
roles: [],
|
||||
fields: [
|
||||
{
|
||||
name: 'Website',
|
||||
|
|
|
@ -398,6 +398,7 @@ function toStories(component: string): string {
|
|||
Promise.all([
|
||||
glob('src/components/global/*.vue'),
|
||||
glob('src/components/MkGalleryPostPreview.vue'),
|
||||
glob('src/pages/user/home.vue'),
|
||||
])
|
||||
.then((globs) => globs.flat())
|
||||
.then((components) => Promise.all(components.map((component) => {
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<MkPagination :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ i18n.ts.notFound }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<MkChannelPreview v-for="item in items" :key="item.id" class="_margin" :channel="extractor(item)"/>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MkChannelPreview from '@/components/MkChannelPreview.vue';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
pagination: Paging;
|
||||
noGap?: boolean;
|
||||
extractor?: (item: any) => any;
|
||||
}>(), {
|
||||
extractor: (item) => item,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
|
@ -82,6 +82,7 @@ export default defineComponent({
|
|||
omitted: null,
|
||||
ignoreOmit: false,
|
||||
defaultStore,
|
||||
i18n,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -439,7 +439,6 @@ defineExpose({
|
|||
|
||||
&.asDrawer {
|
||||
width: 100% !important;
|
||||
padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
|
||||
|
||||
> .emojis {
|
||||
::v-deep(section) {
|
||||
|
@ -498,6 +497,10 @@ defineExpose({
|
|||
background: transparent;
|
||||
color: var(--fg);
|
||||
|
||||
&:not(:focus):not(.filled) {
|
||||
margin-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
|
||||
&:not(.filled) {
|
||||
order: 1;
|
||||
z-index: 2;
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
import { onMounted } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import VuePlyr from 'vue-plyr';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { soundConfigStore } from '@/scripts/sound';
|
||||
import 'vue-plyr/dist/vue-plyr.css';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
|
@ -44,11 +44,11 @@ const audioEl = $shallowRef<HTMLAudioElement | null>();
|
|||
let hide = $ref(true);
|
||||
|
||||
function volumechange() {
|
||||
if (audioEl) ColdDeviceStorage.set('mediaVolume', audioEl.volume);
|
||||
if (audioEl) soundConfigStore.set('mediaVolume', audioEl.volume);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (audioEl) audioEl.volume = ColdDeviceStorage.get('mediaVolume');
|
||||
if (audioEl) audioEl.volume = soundConfigStore.state.mediaVolume;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1124,16 +1124,16 @@ defineExpose({
|
|||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: repeat(auto-fill, minmax(42px, 1fr));
|
||||
grid-auto-rows: 46px;
|
||||
grid-auto-rows: 40px;
|
||||
}
|
||||
|
||||
.footerRight {
|
||||
flex: 0.3;
|
||||
flex: 0;
|
||||
margin-left: auto;
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: repeat(auto-fill, minmax(42px, 1fr));
|
||||
grid-auto-rows: 46px;
|
||||
grid-auto-rows: 40px;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
|
@ -1198,13 +1198,21 @@ defineExpose({
|
|||
}
|
||||
}
|
||||
|
||||
@container (max-width: 330px) {
|
||||
@container (max-width: 350px) {
|
||||
.footer {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.footerLeft {
|
||||
grid-template-columns: repeat(auto-fill, minmax(38px, 1fr));
|
||||
}
|
||||
|
||||
.footerRight {
|
||||
grid-template-columns: repeat(auto-fill, minmax(38px, 1fr));
|
||||
}
|
||||
|
||||
.headerRight {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -83,7 +83,7 @@ const choseAd = (): Ad | null => {
|
|||
};
|
||||
|
||||
const chosen = ref(choseAd());
|
||||
const shouldHide = $ref($i && $i.policies.canHideAds && (props.specify == null));
|
||||
const shouldHide = $ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
|
||||
|
||||
function reduceFrequency(): void {
|
||||
if (chosen.value == null) return;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy';
|
||||
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy';
|
||||
import { defaultStore } from '@/store';
|
||||
import { customEmojis } from '@/custom-emojis';
|
||||
|
||||
|
@ -15,25 +15,38 @@ const props = defineProps<{
|
|||
noStyle?: boolean;
|
||||
host?: string | null;
|
||||
url?: string;
|
||||
useOriginalSize?: boolean;
|
||||
}>();
|
||||
|
||||
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substr(1, props.name.length - 2) : props.name).replace('@.', ''));
|
||||
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
|
||||
|
||||
const rawUrl = computed(() => {
|
||||
if (props.url) {
|
||||
return props.url;
|
||||
}
|
||||
if (props.host == null && !customEmojiName.value.includes('@')) {
|
||||
if (isLocal.value) {
|
||||
return customEmojis.value.find(x => x.name === customEmojiName.value)?.url ?? null;
|
||||
}
|
||||
return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
|
||||
});
|
||||
|
||||
const url = computed(() =>
|
||||
defaultStore.reactiveState.disableShowingAnimatedImages.value && rawUrl.value
|
||||
? getStaticImageUrl(rawUrl.value)
|
||||
: rawUrl.value,
|
||||
);
|
||||
const url = computed(() => {
|
||||
if (rawUrl.value == null) return null;
|
||||
|
||||
const proxied =
|
||||
(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value))
|
||||
? rawUrl.value
|
||||
: getProxiedImageUrl(
|
||||
rawUrl.value,
|
||||
props.useOriginalSize ? undefined : 'emoji',
|
||||
false,
|
||||
true,
|
||||
);
|
||||
return defaultStore.reactiveState.disableShowingAnimatedImages.value
|
||||
? getStaticImageUrl(proxied)
|
||||
: proxied;
|
||||
});
|
||||
|
||||
const alt = computed(() => `:${customEmojiName.value}:`);
|
||||
let errored = $ref(url.value == null);
|
||||
|
|
|
@ -51,6 +51,10 @@ export default defineComponent({
|
|||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
rootScale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
|
@ -65,7 +69,12 @@ export default defineComponent({
|
|||
|
||||
const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
|
||||
|
||||
const genEl = (ast: mfm.MfmNode[]) => ast.map((token): VNode | string | (VNode | string)[] => {
|
||||
/**
|
||||
* Gen Vue Elements from MFM AST
|
||||
* @param ast MFM AST
|
||||
* @param scale How times large the text is
|
||||
*/
|
||||
const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
|
||||
switch (token.type) {
|
||||
case 'text': {
|
||||
const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
|
||||
|
@ -84,17 +93,17 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
case 'bold': {
|
||||
return [h('b', genEl(token.children))];
|
||||
return [h('b', genEl(token.children, scale))];
|
||||
}
|
||||
|
||||
case 'strike': {
|
||||
return [h('del', genEl(token.children))];
|
||||
return [h('del', genEl(token.children, scale))];
|
||||
}
|
||||
|
||||
case 'italic': {
|
||||
return h('i', {
|
||||
style: 'font-style: oblique;',
|
||||
}, genEl(token.children));
|
||||
}, genEl(token.children, scale));
|
||||
}
|
||||
|
||||
case 'fn': {
|
||||
|
@ -155,17 +164,17 @@ export default defineComponent({
|
|||
case 'x2': {
|
||||
return h('span', {
|
||||
class: defaultStore.state.advancedMfm ? 'mfm-x2' : '',
|
||||
}, genEl(token.children));
|
||||
}, genEl(token.children, scale * 2));
|
||||
}
|
||||
case 'x3': {
|
||||
return h('span', {
|
||||
class: defaultStore.state.advancedMfm ? 'mfm-x3' : '',
|
||||
}, genEl(token.children));
|
||||
}, genEl(token.children, scale * 3));
|
||||
}
|
||||
case 'x4': {
|
||||
return h('span', {
|
||||
class: defaultStore.state.advancedMfm ? 'mfm-x4' : '',
|
||||
}, genEl(token.children));
|
||||
}, genEl(token.children, scale * 4));
|
||||
}
|
||||
case 'font': {
|
||||
const family =
|
||||
|
@ -182,7 +191,7 @@ export default defineComponent({
|
|||
case 'blur': {
|
||||
return h('span', {
|
||||
class: '_mfm_blur_',
|
||||
}, genEl(token.children));
|
||||
}, genEl(token.children, scale));
|
||||
}
|
||||
case 'rainbow': {
|
||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
||||
|
@ -191,9 +200,9 @@ export default defineComponent({
|
|||
}
|
||||
case 'sparkle': {
|
||||
if (!useAnim) {
|
||||
return genEl(token.children);
|
||||
return genEl(token.children, scale);
|
||||
}
|
||||
return h(MkSparkle, {}, genEl(token.children));
|
||||
return h(MkSparkle, {}, genEl(token.children, scale));
|
||||
}
|
||||
case 'rotate': {
|
||||
const degrees = parseFloat(token.props.args.deg ?? '90');
|
||||
|
@ -214,7 +223,8 @@ export default defineComponent({
|
|||
}
|
||||
const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5);
|
||||
const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5);
|
||||
style = `transform: scale(${x}, ${y});`;
|
||||
style = `transform: scale(${x}, ${y});`;
|
||||
scale = scale * Math.max(x, y);
|
||||
break;
|
||||
}
|
||||
case 'fg': {
|
||||
|
@ -231,24 +241,24 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
if (style == null) {
|
||||
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']);
|
||||
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
|
||||
} else {
|
||||
return h('span', {
|
||||
style: 'display: inline-block; ' + style,
|
||||
}, genEl(token.children));
|
||||
}, genEl(token.children, scale));
|
||||
}
|
||||
}
|
||||
|
||||
case 'small': {
|
||||
return [h('small', {
|
||||
style: 'opacity: 0.7;',
|
||||
}, genEl(token.children))];
|
||||
}, genEl(token.children, scale))];
|
||||
}
|
||||
|
||||
case 'center': {
|
||||
return [h('div', {
|
||||
style: 'text-align:center;',
|
||||
}, genEl(token.children))];
|
||||
}, genEl(token.children, scale))];
|
||||
}
|
||||
|
||||
case 'url': {
|
||||
|
@ -264,7 +274,7 @@ export default defineComponent({
|
|||
key: Math.random(),
|
||||
url: token.props.url,
|
||||
rel: 'nofollow noopener',
|
||||
}, genEl(token.children))];
|
||||
}, genEl(token.children, scale))];
|
||||
}
|
||||
|
||||
case 'mention': {
|
||||
|
@ -303,11 +313,11 @@ export default defineComponent({
|
|||
if (!this.nowrap) {
|
||||
return [h('div', {
|
||||
style: QUOTE_STYLE,
|
||||
}, genEl(token.children))];
|
||||
}, genEl(token.children, scale))];
|
||||
} else {
|
||||
return [h('span', {
|
||||
style: QUOTE_STYLE,
|
||||
}, genEl(token.children))];
|
||||
}, genEl(token.children, scale))];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -319,6 +329,7 @@ export default defineComponent({
|
|||
name: token.props.name,
|
||||
normal: this.plain,
|
||||
host: null,
|
||||
useOriginalSize: scale >= 2.5,
|
||||
})];
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
|
@ -332,6 +343,7 @@ export default defineComponent({
|
|||
url: this.emojiUrls ? this.emojiUrls[token.props.name] : null,
|
||||
normal: this.plain,
|
||||
host: this.author.host,
|
||||
useOriginalSize: scale >= 2.5,
|
||||
})];
|
||||
}
|
||||
}
|
||||
|
@ -360,7 +372,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
case 'plain': {
|
||||
return [h('span', genEl(token.children))];
|
||||
return [h('span', genEl(token.children, scale))];
|
||||
}
|
||||
|
||||
default: {
|
||||
|
@ -373,6 +385,6 @@ export default defineComponent({
|
|||
}).flat(Infinity) as (VNode | string)[];
|
||||
|
||||
// Parse ast to DOM
|
||||
return h('span', genEl(ast));
|
||||
return h('span', genEl(ast, this.rootScale));
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,6 +2,23 @@
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div v-if="tab === 'search'">
|
||||
<div class="_gaps">
|
||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="searchType" @update:model-value="search()">
|
||||
<option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option>
|
||||
<option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option>
|
||||
</MkRadios>
|
||||
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
|
||||
<MkFoldableSection v-if="channelPagination">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<MkChannelList :key="key" :pagination="channelPagination"/>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
<div v-if="tab === 'featured'">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
||||
|
@ -28,17 +45,35 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import MkChannelPreview from '@/components/MkChannelPreview.vue';
|
||||
import MkChannelList from '@/components/MkChannelList.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { useRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
let tab = $ref('featured');
|
||||
const props = defineProps<{
|
||||
query: string;
|
||||
type?: string;
|
||||
}>();
|
||||
|
||||
let key = $ref('');
|
||||
let tab = $ref('search');
|
||||
let searchQuery = $ref('');
|
||||
let searchType = $ref('nameAndDescription');
|
||||
let channelPagination = $ref();
|
||||
|
||||
onMounted(() => {
|
||||
searchQuery = props.query ?? '';
|
||||
searchType = props.type ?? 'nameAndDescription';
|
||||
});
|
||||
|
||||
const featuredPagination = {
|
||||
endpoint: 'channels/featured' as const,
|
||||
|
@ -58,6 +93,25 @@ const ownedPagination = {
|
|||
limit: 10,
|
||||
};
|
||||
|
||||
async function search() {
|
||||
const query = searchQuery.toString().trim();
|
||||
|
||||
if (query == null || query === '') return;
|
||||
|
||||
const type = searchType.toString().trim();
|
||||
|
||||
channelPagination = {
|
||||
endpoint: 'channels/search',
|
||||
limit: 10,
|
||||
params: {
|
||||
query: searchQuery,
|
||||
type: type,
|
||||
},
|
||||
};
|
||||
|
||||
key = query + type;
|
||||
}
|
||||
|
||||
function create() {
|
||||
router.push('/channels/new');
|
||||
}
|
||||
|
@ -69,6 +123,10 @@ const headerActions = $computed(() => [{
|
|||
}]);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
key: 'search',
|
||||
title: i18n.ts.search,
|
||||
icon: 'ti ti-search',
|
||||
}, {
|
||||
key: 'featured',
|
||||
title: i18n.ts._channel.featured,
|
||||
icon: 'ti ti-comet',
|
||||
|
|
|
@ -66,7 +66,7 @@ const recentPostsPagination = {
|
|||
};
|
||||
const popularPostsPagination = {
|
||||
endpoint: 'gallery/featured' as const,
|
||||
limit: 5,
|
||||
noPaging: true,
|
||||
};
|
||||
const myPostsPagination = {
|
||||
endpoint: 'i/gallery/posts' as const,
|
||||
|
|
|
@ -8,27 +8,29 @@
|
|||
</div>
|
||||
</template>
|
||||
<template #default="{items}">
|
||||
<div v-for="token in items" :key="token.id" class="_panel bfomjevm">
|
||||
<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/>
|
||||
<div class="body">
|
||||
<div class="name">{{ token.name }}</div>
|
||||
<div class="description">{{ token.description }}</div>
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.installedDate }}</template>
|
||||
<template #value><MkTime :time="token.createdAt"/></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.lastUsedDate }}</template>
|
||||
<template #value><MkTime :time="token.lastUsedAt"/></template>
|
||||
</MkKeyValue>
|
||||
<details>
|
||||
<summary>{{ i18n.ts.details }}</summary>
|
||||
<ul>
|
||||
<li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</details>
|
||||
<div class="actions">
|
||||
<MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton>
|
||||
<div class="_gaps">
|
||||
<div v-for="token in items" :key="token.id" class="_panel bfomjevm">
|
||||
<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/>
|
||||
<div class="body">
|
||||
<div class="name">{{ token.name }}</div>
|
||||
<div class="description">{{ token.description }}</div>
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.installedDate }}</template>
|
||||
<template #value><MkTime :time="token.createdAt"/></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.lastUsedDate }}</template>
|
||||
<template #value><MkTime :time="token.lastUsedAt"/></template>
|
||||
</MkKeyValue>
|
||||
<details>
|
||||
<summary>{{ i18n.ts.details }}</summary>
|
||||
<ul>
|
||||
<li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</details>
|
||||
<div class="actions">
|
||||
<MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -51,6 +53,7 @@ const list = ref<any>(null);
|
|||
const pagination = {
|
||||
endpoint: 'i/apps' as const,
|
||||
limit: 100,
|
||||
noPaging: true,
|
||||
params: {
|
||||
sort: '+lastUsedAt',
|
||||
},
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
<MkSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</MkSwitch>
|
||||
<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
|
||||
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
|
||||
<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
|
||||
</div>
|
||||
<div>
|
||||
<MkRadios v-model="emojiStyle">
|
||||
|
@ -163,6 +164,7 @@ const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
|
|||
const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
|
||||
const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
|
||||
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
|
||||
const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
|
||||
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
|
||||
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
|
||||
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
|
||||
|
|
|
@ -400,7 +400,7 @@ function menu(ev: MouseEvent, profileId: string) {
|
|||
icon: 'ti ti-device-floppy',
|
||||
action: () => save(profileId),
|
||||
}, null, {
|
||||
text: ts._preferencesBackups.delete,
|
||||
text: ts.delete,
|
||||
icon: 'ti ti-trash',
|
||||
action: () => deleteProfile(profileId),
|
||||
danger: true,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<FormSection>
|
||||
<template #label>{{ i18n.ts.sounds }}</template>
|
||||
<div class="_gaps_s">
|
||||
<MkFolder v-for="type in Object.keys(sounds)" :key="type">
|
||||
<MkFolder v-for="type in soundsKeys" :key="type">
|
||||
<template #label>{{ i18n.t('_sfx.' + type) }}</template>
|
||||
<template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template>
|
||||
|
||||
|
@ -21,51 +21,44 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { Ref, computed, ref } from 'vue';
|
||||
import XSound from './sounds.sound.vue';
|
||||
import MkRange from '@/components/MkRange.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { soundConfigStore } from '@/scripts/sound';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const masterVolume = computed({
|
||||
get: () => {
|
||||
return ColdDeviceStorage.get('sound_masterVolume');
|
||||
},
|
||||
set: (value) => {
|
||||
ColdDeviceStorage.set('sound_masterVolume', value);
|
||||
},
|
||||
const masterVolume = computed(soundConfigStore.makeGetterSetter('sound_masterVolume'));
|
||||
|
||||
const soundsKeys = ['note', 'noteMy', 'notification', 'chat', 'chatBg', 'antenna', 'channel'] as const;
|
||||
|
||||
const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({
|
||||
note: soundConfigStore.reactiveState.sound_note,
|
||||
noteMy: soundConfigStore.reactiveState.sound_noteMy,
|
||||
notification: soundConfigStore.reactiveState.sound_notification,
|
||||
chat: soundConfigStore.reactiveState.sound_chat,
|
||||
chatBg: soundConfigStore.reactiveState.sound_chatBg,
|
||||
antenna: soundConfigStore.reactiveState.sound_antenna,
|
||||
channel: soundConfigStore.reactiveState.sound_channel,
|
||||
});
|
||||
|
||||
const volumeIcon = computed(() => masterVolume.value === 0 ? 'ti ti-volume-3' : 'ti ti-volume');
|
||||
|
||||
const sounds = ref({
|
||||
note: ColdDeviceStorage.get('sound_note'),
|
||||
noteMy: ColdDeviceStorage.get('sound_noteMy'),
|
||||
notification: ColdDeviceStorage.get('sound_notification'),
|
||||
chat: ColdDeviceStorage.get('sound_chat'),
|
||||
chatBg: ColdDeviceStorage.get('sound_chatBg'),
|
||||
antenna: ColdDeviceStorage.get('sound_antenna'),
|
||||
channel: ColdDeviceStorage.get('sound_channel'),
|
||||
});
|
||||
|
||||
async function updated(type, sound) {
|
||||
async function updated(type: keyof typeof sounds.value, sound) {
|
||||
const v = {
|
||||
type: sound.type,
|
||||
volume: sound.volume,
|
||||
};
|
||||
|
||||
ColdDeviceStorage.set('sound_' + type, v);
|
||||
soundConfigStore.set(`sound_${type}`, v);
|
||||
sounds.value[type] = v;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
for (const sound of Object.keys(sounds.value)) {
|
||||
const v = ColdDeviceStorage.default['sound_' + sound];
|
||||
ColdDeviceStorage.set('sound_' + sound, v);
|
||||
for (const sound of Object.keys(sounds.value) as Array<keyof typeof sounds.value>) {
|
||||
const v = soundConfigStore.def[`sound_${sound}`].default;
|
||||
soundConfigStore.set(`sound_${sound}`, v);
|
||||
sounds.value[sound] = v;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,18 +7,20 @@
|
|||
<FormSection>
|
||||
<MkPagination :pagination="pagination">
|
||||
<template #default="{items}">
|
||||
<FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`" class="_margin">
|
||||
<template #icon>
|
||||
<i v-if="webhook.active === false" class="ti ti-player-pause"></i>
|
||||
<i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i>
|
||||
<i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--success)' }"></i>
|
||||
<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"></i>
|
||||
</template>
|
||||
{{ webhook.name || webhook.url }}
|
||||
<template #suffix>
|
||||
<MkTime v-if="webhook.latestSentAt" :time="webhook.latestSentAt"></MkTime>
|
||||
</template>
|
||||
</FormLink>
|
||||
<div class="_gaps">
|
||||
<FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`">
|
||||
<template #icon>
|
||||
<i v-if="webhook.active === false" class="ti ti-player-pause"></i>
|
||||
<i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i>
|
||||
<i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--success)' }"></i>
|
||||
<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"></i>
|
||||
</template>
|
||||
{{ webhook.name || webhook.url }}
|
||||
<template #suffix>
|
||||
<MkTime v-if="webhook.latestSentAt" :time="webhook.latestSentAt"></MkTime>
|
||||
</template>
|
||||
</FormLink>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</FormSection>
|
||||
|
@ -35,7 +37,8 @@ import { i18n } from '@/i18n';
|
|||
|
||||
const pagination = {
|
||||
endpoint: 'i/webhooks/list' as const,
|
||||
limit: 10,
|
||||
limit: 100,
|
||||
noPaging: true,
|
||||
};
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { userDetailed } from '../../../.storybook/fakes';
|
||||
import { commonHandlers } from '../../../.storybook/mocks';
|
||||
import home_ from './home.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
home_,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<home_ v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
user: userDetailed(),
|
||||
disableNotes: false,
|
||||
},
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users/notes', (req, res, ctx) => {
|
||||
return res(ctx.json([]));
|
||||
}),
|
||||
rest.get('/api/charts/user/notes', (req, res, ctx) => {
|
||||
const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
||||
return res(ctx.json({
|
||||
total: Array.from({ length }, () => 0),
|
||||
inc: Array.from({ length }, () => 0),
|
||||
dec: Array.from({ length }, () => 0),
|
||||
diffs: {
|
||||
normal: Array.from({ length }, () => 0),
|
||||
reply: Array.from({ length }, () => 0),
|
||||
renote: Array.from({ length }, () => 0),
|
||||
withFile: Array.from({ length }, () => 0),
|
||||
},
|
||||
}));
|
||||
}),
|
||||
rest.get('/api/charts/user/pv', (req, res, ctx) => {
|
||||
const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
||||
return res(ctx.json({
|
||||
upv: {
|
||||
user: Array.from({ length }, () => 0),
|
||||
visitor: Array.from({ length }, () => 0),
|
||||
},
|
||||
pv: {
|
||||
user: Array.from({ length }, () => 0),
|
||||
visitor: Array.from({ length }, () => 0),
|
||||
},
|
||||
}));
|
||||
}),
|
||||
],
|
||||
},
|
||||
chromatic: {
|
||||
// `XActivity` is not compatible with Chromatic for now
|
||||
disableSnapshot: true,
|
||||
},
|
||||
},
|
||||
} satisfies StoryObj<typeof home_>;
|
|
@ -2,7 +2,7 @@ import { query } from '@/scripts/url';
|
|||
import { url } from '@/config';
|
||||
import { instance } from '@/instance';
|
||||
|
||||
export function getProxiedImageUrl(imageUrl: string, type?: 'preview', mustOrigin: boolean = false): string {
|
||||
export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin: boolean = false, noFallback: boolean = false): string {
|
||||
const localProxy = `${url}/proxy`;
|
||||
|
||||
if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
|
||||
|
@ -15,7 +15,7 @@ export function getProxiedImageUrl(imageUrl: string, type?: 'preview', mustOrigi
|
|||
: 'image.webp'
|
||||
}?${query({
|
||||
url: imageUrl,
|
||||
fallback: '1',
|
||||
...(!noFallback ? { 'fallback': '1' } : {}),
|
||||
...(type ? { [type]: '1' } : {}),
|
||||
...(mustOrigin ? { origin: '1' } : {}),
|
||||
})}`;
|
||||
|
|
|
@ -1,4 +1,56 @@
|
|||
import { ColdDeviceStorage } from '@/store';
|
||||
import { markRaw } from 'vue';
|
||||
import { Storage } from '@/pizzax';
|
||||
|
||||
export const soundConfigStore = markRaw(new Storage('sound', {
|
||||
mediaVolume: {
|
||||
where: 'device',
|
||||
default: 0.5
|
||||
},
|
||||
sound_masterVolume: {
|
||||
where: 'device',
|
||||
default: 0.3
|
||||
},
|
||||
sound_note: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/n-aec', volume: 1 }
|
||||
},
|
||||
sound_noteMy: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/n-cea-4va', volume: 1 }
|
||||
},
|
||||
sound_notification: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/n-ea', volume: 1 }
|
||||
},
|
||||
sound_chat: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/pope1', volume: 1 }
|
||||
},
|
||||
sound_chatBg: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/waon', volume: 1 }
|
||||
},
|
||||
sound_antenna: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/triple', volume: 1 }
|
||||
},
|
||||
sound_channel: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/square-pico', volume: 1 }
|
||||
},
|
||||
}));
|
||||
|
||||
await soundConfigStore.ready;
|
||||
|
||||
//#region サウンドのColdDeviceStorage => indexedDBのマイグレーション
|
||||
for (const target of Object.keys(soundConfigStore.state) as Array<keyof typeof soundConfigStore.state>) {
|
||||
const value = localStorage.getItem(`miux:${target}`);
|
||||
if (value) {
|
||||
soundConfigStore.set(target, JSON.parse(value) as typeof soundConfigStore.def[typeof target]['default']);
|
||||
localStorage.removeItem(`miux:${target}`);
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const cache = new Map<string, HTMLAudioElement>();
|
||||
|
||||
|
@ -67,19 +119,20 @@ export function getAudio(file: string, useCache = true): HTMLAudioElement {
|
|||
}
|
||||
|
||||
export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement {
|
||||
const masterVolume = ColdDeviceStorage.get('sound_masterVolume');
|
||||
const masterVolume = soundConfigStore.state.sound_masterVolume;
|
||||
audio.volume = masterVolume - ((1 - volume) * masterVolume);
|
||||
return audio;
|
||||
}
|
||||
|
||||
export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') {
|
||||
const sound = ColdDeviceStorage.get(`sound_${type}`);
|
||||
const sound = soundConfigStore.state[`sound_${type}`];
|
||||
if (_DEV_) console.log('play', type, sound);
|
||||
if (sound.type == null) return;
|
||||
playFile(sound.type, sound.volume);
|
||||
}
|
||||
|
||||
export function playFile(file: string, volume: number) {
|
||||
const masterVolume = ColdDeviceStorage.get('sound_masterVolume');
|
||||
const masterVolume = soundConfigStore.state.sound_masterVolume;
|
||||
if (masterVolume === 0) return;
|
||||
|
||||
const audio = setVolume(getAudio(file), volume);
|
||||
|
|
|
@ -298,6 +298,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
forceShowAds: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
aiChanMode: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
|
@ -343,15 +347,6 @@ export class ColdDeviceStorage {
|
|||
darkTheme,
|
||||
syncDeviceDarkMode: true,
|
||||
plugins: [] as Plugin[],
|
||||
mediaVolume: 0.5,
|
||||
sound_masterVolume: 0.5,
|
||||
sound_note: { type: 'syuilo/n-eca', volume: 0.5 },
|
||||
sound_noteMy: { type: 'syuilo/n-cea-4va', volume: 0.5 },
|
||||
sound_notification: { type: 'syuilo/n-ea', volume: 0.5 },
|
||||
sound_chat: { type: 'syuilo/pope1', volume: 0.5 },
|
||||
sound_chatBg: { type: 'syuilo/waon', volume: 0.5 },
|
||||
sound_antenna: { type: 'syuilo/triple', volume: 0.5 },
|
||||
sound_channel: { type: 'syuilo/square-pico', volume: 0.5 },
|
||||
};
|
||||
|
||||
public static watchers: Watcher[] = [];
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import { post } from '@/os';
|
||||
import { api, post } from '@/os';
|
||||
import { $i, login } from '@/account';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
import { mainRouter } from '@/router';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
export function swInject() {
|
||||
navigator.serviceWorker.addEventListener('message', ev => {
|
||||
navigator.serviceWorker.addEventListener('message', async ev => {
|
||||
if (_DEV_) {
|
||||
console.log('sw msg', ev.data);
|
||||
}
|
||||
|
||||
if (ev.data.type !== 'order') return;
|
||||
|
||||
if (ev.data.loginId !== $i?.id) {
|
||||
if (ev.data.loginId && ev.data.loginId !== $i?.id) {
|
||||
return getAccountFromId(ev.data.loginId).then(account => {
|
||||
if (!account) return;
|
||||
return login(account.token, ev.data.url);
|
||||
|
@ -19,8 +20,18 @@ export function swInject() {
|
|||
}
|
||||
|
||||
switch (ev.data.order) {
|
||||
case 'post':
|
||||
return post(ev.data.options);
|
||||
case 'post': {
|
||||
const props = deepClone(ev.data.options);
|
||||
// プッシュ通知から来たreply,renoteはtruncateBodyが通されているため、
|
||||
// 完全なノートを取得しなおす
|
||||
if (props.reply) {
|
||||
props.reply = await api('notes/show', { noteId: props.reply.id });
|
||||
}
|
||||
if (props.renote) {
|
||||
props.renote = await api('notes/show', { noteId: props.renote.id });
|
||||
}
|
||||
return post(props);
|
||||
}
|
||||
case 'push':
|
||||
if (mainRouter.currentRoute.value.path === ev.data.url) {
|
||||
return window.scroll({ top: 0, behavior: 'smooth' });
|
||||
|
|
|
@ -250,6 +250,7 @@ onMounted(() => {
|
|||
> .widgets {
|
||||
//--panelBorder: none;
|
||||
width: 300px;
|
||||
padding-bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px));
|
||||
|
||||
@media (max-width: $widgets-hide-threshold) {
|
||||
display: none;
|
||||
|
@ -304,7 +305,7 @@ onMounted(() => {
|
|||
right: 0;
|
||||
z-index: 1001;
|
||||
height: 100dvh;
|
||||
padding: var(--margin);
|
||||
padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
background: var(--bg);
|
||||
|
|
|
@ -296,7 +296,7 @@ $widgets-hide-threshold: 1090px;
|
|||
}
|
||||
|
||||
.widgets {
|
||||
padding: 0 var(--margin);
|
||||
padding: 0 var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
|
||||
border-left: solid 0.5px var(--divider);
|
||||
background: var(--bg);
|
||||
|
||||
|
@ -329,7 +329,7 @@ $widgets-hide-threshold: 1090px;
|
|||
right: 0;
|
||||
z-index: 1001;
|
||||
height: 100dvh;
|
||||
padding: var(--margin) !important;
|
||||
padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)) !important;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<XWidgets :class="$style.widgets" :edit="editMode" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
|
||||
|
||||
<button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button>
|
||||
<button v-else class="_textButton mk-widget-edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button>
|
||||
<button v-else class="_textButton mk-widget-edit" :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -91,4 +91,8 @@ function updateWidgets(thisWidgets) {
|
|||
.widgets {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.edit {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
"node": false
|
||||
node: false,
|
||||
},
|
||||
parserOptions: {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
parser: '@typescript-eslint/parser',
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
extends: [
|
||||
"../shared/.eslintrc.js",
|
||||
],
|
||||
extends: ['../shared/.eslintrc.js'],
|
||||
globals: {
|
||||
"require": false,
|
||||
"_DEV_": false,
|
||||
"_LANGS_": false,
|
||||
"_VERSION_": false,
|
||||
"_ENV_": false,
|
||||
"_PERF_PREFIX_": false,
|
||||
}
|
||||
}
|
||||
require: false,
|
||||
_DEV_: false,
|
||||
_LANGS_: false,
|
||||
_VERSION_: false,
|
||||
_ENV_: false,
|
||||
_PERF_PREFIX_: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @ts-check
|
||||
|
||||
const esbuild = require('esbuild');
|
||||
const locales = require('../../locales');
|
||||
const meta = require('../../package.json');
|
||||
|
@ -5,33 +7,36 @@ const watch = process.argv[2]?.includes('watch');
|
|||
|
||||
console.log('Starting SW building...');
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: [ `${__dirname}/src/sw.ts` ],
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
treeShaking: true,
|
||||
minify: process.env.NODE_ENV === 'production',
|
||||
/** @type {esbuild.BuildOptions} */
|
||||
const buildOptions = {
|
||||
absWorkingDir: __dirname,
|
||||
bundle: true,
|
||||
define: {
|
||||
_DEV_: JSON.stringify(process.env.NODE_ENV !== 'production'),
|
||||
_ENV_: JSON.stringify(process.env.NODE_ENV ?? ''), // `NODE_ENV`が`undefined`なとき`JSON.stringify`が`undefined`を返してエラーになってしまうので`??`を使っている
|
||||
_LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])),
|
||||
_PERF_PREFIX_: JSON.stringify('Misskey:'),
|
||||
_VERSION_: JSON.stringify(meta.version),
|
||||
},
|
||||
entryPoints: [`${__dirname}/src/sw.ts`],
|
||||
format: 'esm',
|
||||
loader: {
|
||||
'.ts': 'ts',
|
||||
},
|
||||
minify: process.env.NODE_ENV === 'production',
|
||||
outbase: `${__dirname}/src`,
|
||||
outdir: `${__dirname}/../../built/_sw_dist_`,
|
||||
loader: {
|
||||
'.ts': 'ts'
|
||||
},
|
||||
treeShaking: true,
|
||||
tsconfig: `${__dirname}/tsconfig.json`,
|
||||
define: {
|
||||
_VERSION_: JSON.stringify(meta.version),
|
||||
_LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])),
|
||||
_ENV_: JSON.stringify(process.env.NODE_ENV),
|
||||
_DEV_: process.env.NODE_ENV !== 'production',
|
||||
_PERF_PREFIX_: JSON.stringify('Misskey:'),
|
||||
},
|
||||
watch: watch ? {
|
||||
onRebuild(error, result) {
|
||||
if (error) console.error('SW: watch build failed:', error);
|
||||
else console.log('SW: watch build succeeded:', result);
|
||||
},
|
||||
} : false,
|
||||
}).then(result => {
|
||||
if (watch) console.log('watching...');
|
||||
else console.log('done,', JSON.stringify(result));
|
||||
});
|
||||
};
|
||||
|
||||
(async () => {
|
||||
if (!watch) {
|
||||
await esbuild.build(buildOptions);
|
||||
console.log('done');
|
||||
} else {
|
||||
const context = await esbuild.context(buildOptions);
|
||||
await context.watch();
|
||||
console.log('watching...');
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"esbuild": "0.14.42",
|
||||
"esbuild": "0.17.15",
|
||||
"idb-keyval": "6.2.0",
|
||||
"misskey-js": "workspace:*"
|
||||
},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type FIXME = any;
|
||||
|
||||
declare const _LANGS_: string[][];
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import * as misskey from 'misskey-js';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
|
||||
export const acct = (user: misskey.Acct) => {
|
||||
return Acct.toString(user);
|
||||
};
|
||||
|
||||
export const userName = (user: misskey.entities.User) => {
|
||||
return user.name || user.username;
|
||||
};
|
||||
|
||||
export const userPage = (user: misskey.Acct, path?, absolute = false) => {
|
||||
return `${absolute ? origin : ''}/@${acct(user)}${(path ? `/${path}` : '')}`;
|
||||
};
|
|
@ -1,31 +1,29 @@
|
|||
/*
|
||||
* Notification manager for SW
|
||||
*/
|
||||
import { swLang } from '@/scripts/lang';
|
||||
import { cli } from '@/scripts/operations';
|
||||
import { badgeNames, pushNotificationDataMap } from '@/types';
|
||||
import getUserName from '@/scripts/get-user-name';
|
||||
import { I18n } from '@/scripts/i18n';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
import type { BadgeNames, PushNotificationDataMap } from '@/types';
|
||||
import { char2fileName } from '@/scripts/twemoji-base';
|
||||
import * as url from '@/scripts/url';
|
||||
import { cli } from '@/scripts/operations';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
import { swLang } from '@/scripts/lang';
|
||||
import { getUserName } from '@/scripts/get-user-name';
|
||||
|
||||
const closeNotificationsByTags = async (tags: string[]) => {
|
||||
const closeNotificationsByTags = async (tags: string[]): Promise<void> => {
|
||||
for (const n of (await Promise.all(tags.map(tag => globalThis.registration.getNotifications({ tag })))).flat()) {
|
||||
n.close();
|
||||
}
|
||||
};
|
||||
|
||||
const iconUrl = (name: badgeNames) => `/static-assets/tabler-badges/${name}.png`;
|
||||
const iconUrl = (name: BadgeNames): string => `/static-assets/tabler-badges/${name}.png`;
|
||||
/* How to add a new badge:
|
||||
* 1. Find the icon and download png from https://tabler-icons.io/
|
||||
* 2. vips resize ~/Downloads/icon-name.png vipswork.png 0.4; vips scRGB2BW vipswork.png ~/icon-name.png"[compression=9,strip]"; rm vipswork.png;
|
||||
* 3. mv ~/icon-name.png ~/misskey/packages/backend/assets/tabler-badges/
|
||||
* 4. Add 'icon-name' to badgeNames
|
||||
* 4. Add 'icon-name' to BadgeNames
|
||||
* 5. Add `badge: iconUrl('icon-name'),`
|
||||
*/
|
||||
|
||||
export async function createNotification<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]) {
|
||||
export async function createNotification<K extends keyof PushNotificationDataMap>(data: PushNotificationDataMap[K]): Promise<void> {
|
||||
const n = await composeNotification(data);
|
||||
|
||||
if (n) {
|
||||
|
@ -36,9 +34,8 @@ export async function createNotification<K extends keyof pushNotificationDataMap
|
|||
}
|
||||
}
|
||||
|
||||
async function composeNotification(data: pushNotificationDataMap[keyof pushNotificationDataMap]): Promise<[string, NotificationOptions] | null> {
|
||||
if (!swLang.i18n) swLang.fetchLocale();
|
||||
const i18n = await swLang.i18n as I18n<any>;
|
||||
async function composeNotification(data: PushNotificationDataMap[keyof PushNotificationDataMap]): Promise<[string, NotificationOptions] | null> {
|
||||
const i18n = await (swLang.i18n ?? swLang.fetchLocale());
|
||||
const { t } = i18n;
|
||||
switch (data.type) {
|
||||
/*
|
||||
|
@ -139,16 +136,16 @@ async function composeNotification(data: pushNotificationDataMap[keyof pushNotif
|
|||
if (reaction.startsWith(':')) {
|
||||
// カスタム絵文字の場合
|
||||
const name = reaction.substring(1, reaction.length - 1);
|
||||
badge = `${origin}/emoji/${name}.webp?${url.query({
|
||||
badge: '1',
|
||||
})}`;
|
||||
const badgeUrl = new URL(`/emoji/${name}.webp`, origin);
|
||||
badgeUrl.searchParams.set('badge', '1');
|
||||
badge = badgeUrl.href;
|
||||
reaction = name.split('@')[0];
|
||||
} else {
|
||||
// Unicode絵文字の場合
|
||||
badge = `/twemoji-badge/${char2fileName(reaction)}.png`;
|
||||
}
|
||||
|
||||
if (badge ? await fetch(badge).then(res => res.status !== 200).catch(() => true) : true) {
|
||||
if (await fetch(badge).then(res => res.status !== 200).catch(() => true)) {
|
||||
badge = iconUrl('plus');
|
||||
}
|
||||
|
||||
|
@ -168,14 +165,6 @@ async function composeNotification(data: pushNotificationDataMap[keyof pushNotif
|
|||
}];
|
||||
}
|
||||
|
||||
case 'pollEnded':
|
||||
return [t('_notification.pollEnded'), {
|
||||
body: data.body.note.text || '',
|
||||
badge: iconUrl('chart-arrows'),
|
||||
tag: `poll:${data.body.note.id}`,
|
||||
data,
|
||||
}];
|
||||
|
||||
case 'receiveFollowRequest':
|
||||
return [t('_notification.youReceivedFollowRequest'), {
|
||||
body: getUserName(data.body.user),
|
||||
|
@ -202,6 +191,14 @@ async function composeNotification(data: pushNotificationDataMap[keyof pushNotif
|
|||
data,
|
||||
}];
|
||||
|
||||
case 'achievementEarned':
|
||||
return [t('_notification.achievementEarned'), {
|
||||
body: t(`_achievements._types._${data.body.achievement}.title`),
|
||||
badge: iconUrl('medal'),
|
||||
data,
|
||||
tag: `achievement:${data.body.achievement}`,
|
||||
}];
|
||||
|
||||
case 'app':
|
||||
return [data.body.header ?? data.body.body, {
|
||||
body: data.body.header ? data.body.body : '',
|
||||
|
@ -226,24 +223,35 @@ async function composeNotification(data: pushNotificationDataMap[keyof pushNotif
|
|||
}
|
||||
}
|
||||
|
||||
export async function createEmptyNotification() {
|
||||
export async function createEmptyNotification(): Promise<void> {
|
||||
return new Promise<void>(async res => {
|
||||
if (!swLang.i18n) swLang.fetchLocale();
|
||||
const i18n = await swLang.i18n as I18n<any>;
|
||||
const i18n = await (swLang.i18n ?? swLang.fetchLocale());
|
||||
const { t } = i18n;
|
||||
|
||||
await globalThis.registration.showNotification(
|
||||
t('_notification.emptyPushNotificationMessage'),
|
||||
(new URL(origin)).host,
|
||||
{
|
||||
body: `Misskey v${_VERSION_}`,
|
||||
silent: true,
|
||||
badge: iconUrl('null'),
|
||||
tag: 'read_notification',
|
||||
actions: [
|
||||
{
|
||||
action: 'markAllAsRead',
|
||||
title: t('markAllAsRead'),
|
||||
},
|
||||
{
|
||||
action: 'settings',
|
||||
title: t('notificationSettings'),
|
||||
},
|
||||
],
|
||||
data: {},
|
||||
},
|
||||
);
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await closeNotificationsByTags(['user_visible_auto_notification', 'read_notification']);
|
||||
await closeNotificationsByTags(['user_visible_auto_notification']);
|
||||
} finally {
|
||||
res();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { get } from 'idb-keyval';
|
||||
|
||||
export async function getAccountFromId(id: string) {
|
||||
const accounts = await get('accounts') as { token: string; id: string; }[];
|
||||
if (!accounts) console.log('Accounts are not recorded');
|
||||
export async function getAccountFromId(id: string): Promise<{ token: string; id: string } | void> {
|
||||
const accounts = await get<{ token: string; id: string }[]>('accounts');
|
||||
if (!accounts) {
|
||||
console.log('Accounts are not recorded');
|
||||
return;
|
||||
}
|
||||
return accounts.find(e => e.id === id);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export default function(user: { name?: string | null, username: string }): string {
|
||||
export function getUserName(user: { name?: string | null; username: string }): string {
|
||||
return user.name === '' ? user.username : user.name ?? user.username;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export class I18n<T extends Record<string, any>> {
|
||||
export type Locale = { [key: string]: string | Locale };
|
||||
|
||||
export class I18n<T extends Locale = Locale> {
|
||||
public ts: T;
|
||||
|
||||
constructor(locale: T) {
|
||||
|
@ -13,7 +15,8 @@ export class I18n<T extends Record<string, any>> {
|
|||
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
|
||||
public t(key: string, args?: Record<string, string>): string {
|
||||
try {
|
||||
let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string;
|
||||
let str = key.split('.').reduce<Locale | Locale[keyof Locale]>((o, i) => o[i], this.ts);
|
||||
if (typeof str !== 'string') throw new Error();
|
||||
|
||||
if (args) {
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Language manager for SW
|
||||
*/
|
||||
import { get, set } from 'idb-keyval';
|
||||
import { I18n } from '@/scripts/i18n';
|
||||
import { I18n, type Locale } from '@/scripts/i18n';
|
||||
|
||||
class SwLang {
|
||||
public cacheName = `mk-cache-${_VERSION_}`;
|
||||
|
@ -12,19 +12,19 @@ class SwLang {
|
|||
return prelang;
|
||||
});
|
||||
|
||||
public setLang(newLang: string) {
|
||||
public setLang(newLang: string): Promise<I18n<Locale>> {
|
||||
this.lang = Promise.resolve(newLang);
|
||||
set('lang', newLang);
|
||||
return this.fetchLocale();
|
||||
}
|
||||
|
||||
public i18n: Promise<I18n<any>> | null = null;
|
||||
public i18n: Promise<I18n> | null = null;
|
||||
|
||||
public fetchLocale() {
|
||||
return this.i18n = this._fetch();
|
||||
public fetchLocale(): Promise<I18n<Locale>> {
|
||||
return (this.i18n = this._fetch());
|
||||
}
|
||||
|
||||
private async _fetch() {
|
||||
private async _fetch(): Promise<I18n<Locale>> {
|
||||
// Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う
|
||||
const localeUrl = `/assets/locales/${await this.lang}.${_VERSION_}.json`;
|
||||
let localeRes = await caches.match(localeUrl);
|
||||
|
@ -32,13 +32,13 @@ class SwLang {
|
|||
// _DEV_がtrueの場合は常に最新化
|
||||
if (!localeRes || _DEV_) {
|
||||
localeRes = await fetch(localeUrl);
|
||||
const clone = localeRes?.clone();
|
||||
if (!clone?.clone().ok) Error('locale fetching error');
|
||||
const clone = localeRes.clone();
|
||||
if (!clone.clone().ok) throw new Error('locale fetching error');
|
||||
|
||||
caches.open(this.cacheName).then(cache => cache.put(localeUrl, clone));
|
||||
}
|
||||
|
||||
return new I18n(await localeRes.json());
|
||||
return new I18n<Locale>(await localeRes.json());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
export function getUrlWithLoginId(url: string, loginId: string) {
|
||||
export function getUrlWithLoginId(url: string, loginId: string): string {
|
||||
const u = new URL(url, origin);
|
||||
u.searchParams.append('loginId', loginId);
|
||||
return u.toString();
|
||||
}
|
||||
|
||||
export function getUrlWithoutLoginId(url: string) {
|
||||
const u = new URL(url);
|
||||
u.searchParams.delete('loginId');
|
||||
u.searchParams.set('loginId', loginId);
|
||||
return u.toString();
|
||||
}
|
||||
|
|
|
@ -3,63 +3,77 @@
|
|||
* 各種操作
|
||||
*/
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { SwMessage, swMessageOrderType } from '@/types';
|
||||
import { acct as getAcct } from '@/filters/user';
|
||||
import type { SwMessage, SwMessageOrderType } from '@/types';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
import { getUrlWithLoginId } from '@/scripts/login-id';
|
||||
|
||||
export const cli = new Misskey.api.APIClient({ origin, fetch: (...args) => fetch(...args) });
|
||||
export const cli = new Misskey.api.APIClient({ origin, fetch: (...args): Promise<Response> => fetch(...args) });
|
||||
|
||||
export async function api<E extends keyof Misskey.Endpoints>(endpoint: E, userId: string, options?: Misskey.Endpoints[E]['req']) {
|
||||
const account = await getAccountFromId(userId);
|
||||
if (!account) return;
|
||||
export async function api<E extends keyof Misskey.Endpoints, O extends Misskey.Endpoints[E]['req']>(endpoint: E, userId?: string, options?: O): Promise<void | ReturnType<typeof cli.request<E, O>>> {
|
||||
let account: { token: string; id: string } | void;
|
||||
|
||||
return cli.request(endpoint, options, account.token);
|
||||
if (userId) {
|
||||
account = await getAccountFromId(userId);
|
||||
if (!account) return;
|
||||
}
|
||||
|
||||
return cli.request(endpoint, options, account?.token);
|
||||
}
|
||||
|
||||
// mark-all-as-read送出を1秒間隔に制限する
|
||||
const readBlockingStatus = new Map<string, boolean>();
|
||||
export function sendMarkAllAsRead(userId: string): Promise<null | undefined | void> {
|
||||
if (readBlockingStatus.get(userId)) return Promise.resolve();
|
||||
readBlockingStatus.set(userId, true);
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
readBlockingStatus.set(userId, false);
|
||||
api('notifications/mark-all-as-read', userId).then(resolve, resolve);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// rendered acctからユーザーを開く
|
||||
export function openUser(acct: string, loginId: string) {
|
||||
export function openUser(acct: string, loginId?: string): ReturnType<typeof openClient> {
|
||||
return openClient('push', `/@${acct}`, loginId, { acct });
|
||||
}
|
||||
|
||||
// noteIdからノートを開く
|
||||
export function openNote(noteId: string, loginId: string) {
|
||||
export function openNote(noteId: string, loginId?: string): ReturnType<typeof openClient> {
|
||||
return openClient('push', `/notes/${noteId}`, loginId, { noteId });
|
||||
}
|
||||
|
||||
// noteIdからノートを開く
|
||||
export function openAntenna(antennaId: string, loginId: string) {
|
||||
export function openAntenna(antennaId: string, loginId: string): ReturnType<typeof openClient> {
|
||||
return openClient('push', `/timeline/antenna/${antennaId}`, loginId, { antennaId });
|
||||
}
|
||||
|
||||
// post-formのオプションから投稿フォームを開く
|
||||
export async function openPost(options: any, loginId: string) {
|
||||
export async function openPost(options: { initialText?: string; reply?: Misskey.entities.Note; renote?: Misskey.entities.Note }, loginId?: string): ReturnType<typeof openClient> {
|
||||
// クエリを作成しておく
|
||||
let url = '/share?';
|
||||
if (options.initialText) url += `text=${options.initialText}&`;
|
||||
if (options.reply) url += `replyId=${options.reply.id}&`;
|
||||
if (options.renote) url += `renoteId=${options.renote.id}&`;
|
||||
const url = '/share';
|
||||
const query = new URLSearchParams();
|
||||
if (options.initialText) query.set('text', options.initialText);
|
||||
if (options.reply) query.set('replyId', options.reply.id);
|
||||
if (options.renote) query.set('renoteId', options.renote.id);
|
||||
|
||||
return openClient('post', url, loginId, { options });
|
||||
return openClient('post', `${url}?${query}`, loginId, { options });
|
||||
}
|
||||
|
||||
export async function openClient(order: swMessageOrderType, url: string, loginId: string, query: any = {}) {
|
||||
export async function openClient(order: SwMessageOrderType, url: string, loginId?: string, query: Record<string, SwMessage[string]> = {}): Promise<WindowClient | null> {
|
||||
const client = await findClient();
|
||||
|
||||
if (client) {
|
||||
client.postMessage({ type: 'order', ...query, order, loginId, url } as SwMessage);
|
||||
client.postMessage({ type: 'order', ...query, order, loginId, url } satisfies SwMessage);
|
||||
return client;
|
||||
}
|
||||
|
||||
return globalThis.clients.openWindow(getUrlWithLoginId(url, loginId));
|
||||
return globalThis.clients.openWindow(loginId ? getUrlWithLoginId(url, loginId) : url);
|
||||
}
|
||||
|
||||
export async function findClient() {
|
||||
export async function findClient(): Promise<WindowClient | null> {
|
||||
const clients = await globalThis.clients.matchAll({
|
||||
type: 'window',
|
||||
});
|
||||
for (const c of clients) {
|
||||
if (!new URL(c.url).searchParams.has('zen')) return c;
|
||||
}
|
||||
return null;
|
||||
return clients.find(c => !(new URL(c.url)).searchParams.has('zen')) ?? null;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
export const twemojiSvgBase = '/twemoji';
|
||||
|
||||
export function char2fileName(char: string): string {
|
||||
let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16));
|
||||
let codes = Array.from(char)
|
||||
.map(x => x.codePointAt(0)?.toString(16))
|
||||
.filter(<T>(x: T | undefined): x is T => x !== undefined);
|
||||
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
|
||||
codes = codes.filter(x => x && x.length);
|
||||
codes = codes.filter(x => x.length !== 0);
|
||||
return codes.join('-');
|
||||
}
|
||||
|
||||
export function char2filePath(char: string): string {
|
||||
return `${twemojiSvgBase}/${char2fileName(char)}.svg`;
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
/* objを検査して
|
||||
* 1. 配列に何も入っていない時はクエリを付けない
|
||||
* 2. プロパティがundefinedの時はクエリを付けない
|
||||
* (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない)
|
||||
*/
|
||||
export function query(obj: object): string {
|
||||
const params = Object.entries(obj)
|
||||
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
|
||||
.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);
|
||||
|
||||
return Object.entries(params)
|
||||
.map((e) => `${e[0]}=${encodeURIComponent(e[1])}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
export function appendQuery(url: string, query: string): string {
|
||||
return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
import { get } from 'idb-keyval';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import type { PushNotificationDataMap } from '@/types';
|
||||
import { createEmptyNotification, createNotification } from '@/scripts/create-notification';
|
||||
import { swLang } from '@/scripts/lang';
|
||||
import { api } from '@/scripts/operations';
|
||||
import { pushNotificationDataMap } from '@/types';
|
||||
import * as swos from '@/scripts/operations';
|
||||
import { acct as getAcct } from '@/filters/user';
|
||||
|
||||
globalThis.addEventListener('install', ev => {
|
||||
//ev.waitUntil(globalThis.skipWaiting());
|
||||
globalThis.addEventListener('install', () => {
|
||||
// ev.waitUntil(globalThis.skipWaiting());
|
||||
});
|
||||
|
||||
globalThis.addEventListener('activate', ev => {
|
||||
|
@ -43,8 +43,8 @@ globalThis.addEventListener('push', ev => {
|
|||
ev.waitUntil(globalThis.clients.matchAll({
|
||||
includeUncontrolled: true,
|
||||
type: 'window',
|
||||
}).then(async (clients: readonly WindowClient[]) => {
|
||||
const data: pushNotificationDataMap[keyof pushNotificationDataMap] = ev.data?.json();
|
||||
}).then(async () => {
|
||||
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json();
|
||||
|
||||
switch (data.type) {
|
||||
// case 'driveFileCreated':
|
||||
|
@ -54,6 +54,10 @@ globalThis.addEventListener('push', ev => {
|
|||
if ((new Date()).getTime() - data.dateTime > 1000 * 60 * 60 * 24) break;
|
||||
|
||||
return createNotification(data);
|
||||
case 'readAllNotifications':
|
||||
await globalThis.registration.getNotifications()
|
||||
.then(notifications => notifications.forEach(n => n.close()));
|
||||
break;
|
||||
}
|
||||
|
||||
await createEmptyNotification();
|
||||
|
@ -62,13 +66,13 @@ globalThis.addEventListener('push', ev => {
|
|||
});
|
||||
|
||||
globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => {
|
||||
ev.waitUntil((async () => {
|
||||
ev.waitUntil((async (): Promise<void> => {
|
||||
if (_DEV_) {
|
||||
console.log('notificationclick', ev.action, ev.notification.data);
|
||||
}
|
||||
|
||||
const { action, notification } = ev;
|
||||
const data: pushNotificationDataMap[keyof pushNotificationDataMap] = notification.data;
|
||||
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {};
|
||||
const { userId: loginId } = data;
|
||||
let client: WindowClient | null = null;
|
||||
|
||||
|
@ -79,7 +83,7 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
|
|||
if ('userId' in data.body) await swos.api('following/create', loginId, { userId: data.body.userId });
|
||||
break;
|
||||
case 'showUser':
|
||||
if ('user' in data.body) client = await swos.openUser(getAcct(data.body.user), loginId);
|
||||
if ('user' in data.body) client = await swos.openUser(Acct.toString(data.body.user), loginId);
|
||||
break;
|
||||
case 'reply':
|
||||
if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId);
|
||||
|
@ -116,7 +120,7 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
|
|||
if ('note' in data.body) {
|
||||
client = await swos.openNote(data.body.note.id, loginId);
|
||||
} else if ('user' in data.body) {
|
||||
client = await swos.openUser(getAcct(data.body.user), loginId);
|
||||
client = await swos.openUser(Acct.toString(data.body.user), loginId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -124,13 +128,29 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
|
|||
break;
|
||||
case 'unreadAntennaNote':
|
||||
client = await swos.openAntenna(data.body.antenna.id, loginId);
|
||||
break;
|
||||
default:
|
||||
switch (action) {
|
||||
case 'markAllAsRead':
|
||||
await globalThis.registration.getNotifications()
|
||||
.then(notifications => notifications.forEach(n => n.close()));
|
||||
await get('accounts').then(accounts => {
|
||||
return Promise.all(accounts.map(async account => {
|
||||
await swos.sendMarkAllAsRead(account.id);
|
||||
}));
|
||||
});
|
||||
break;
|
||||
case 'settings':
|
||||
client = await swos.openClient('push', '/settings/notifications', loginId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (client) {
|
||||
client.focus();
|
||||
}
|
||||
if (data.type === 'notification') {
|
||||
api('notifications/mark-all-as-read', data.userId);
|
||||
await swos.sendMarkAllAsRead(loginId);
|
||||
}
|
||||
|
||||
notification.close();
|
||||
|
@ -138,15 +158,18 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
|
|||
});
|
||||
|
||||
globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => {
|
||||
const data: pushNotificationDataMap[keyof pushNotificationDataMap] = ev.notification.data;
|
||||
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data;
|
||||
|
||||
if (data.type === 'notification') {
|
||||
api('notifications/mark-all-as-read', data.userId);
|
||||
}
|
||||
ev.waitUntil((async (): Promise<void> => {
|
||||
if (data.type === 'notification') {
|
||||
await swos.sendMarkAllAsRead(data.userId);
|
||||
}
|
||||
return;
|
||||
})());
|
||||
});
|
||||
|
||||
globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => {
|
||||
ev.waitUntil((async () => {
|
||||
ev.waitUntil((async (): Promise<void> => {
|
||||
switch (ev.data) {
|
||||
case 'clear':
|
||||
// Cache Storage全削除
|
||||
|
|
|
@ -1,46 +1,47 @@
|
|||
import * as Misskey from 'misskey-js';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
|
||||
export type swMessageOrderType = 'post' | 'push';
|
||||
export type SwMessageOrderType = 'post' | 'push';
|
||||
|
||||
export type SwMessage = {
|
||||
type: 'order';
|
||||
order: swMessageOrderType;
|
||||
loginId: string;
|
||||
order: SwMessageOrderType;
|
||||
loginId?: string;
|
||||
url: string;
|
||||
[x: string]: any;
|
||||
[x: string]: unknown;
|
||||
};
|
||||
|
||||
// Defined also @/core/PushNotificationService.ts#L12
|
||||
type pushNotificationDataSourceMap = {
|
||||
type PushNotificationDataSourceMap = {
|
||||
notification: Misskey.entities.Notification;
|
||||
unreadAntennaNote: {
|
||||
antenna: { id: string, name: string };
|
||||
antenna: { id: string; name: string };
|
||||
note: Misskey.entities.Note;
|
||||
};
|
||||
readAllNotifications: undefined;
|
||||
};
|
||||
|
||||
export type pushNotificationData<K extends keyof pushNotificationDataSourceMap> = {
|
||||
export type PushNotificationData<K extends keyof PushNotificationDataSourceMap> = {
|
||||
type: K;
|
||||
body: pushNotificationDataSourceMap[K];
|
||||
body: PushNotificationDataSourceMap[K];
|
||||
userId: string;
|
||||
dateTime: number;
|
||||
};
|
||||
|
||||
export type pushNotificationDataMap = {
|
||||
[K in keyof pushNotificationDataSourceMap]: pushNotificationData<K>;
|
||||
export type PushNotificationDataMap = {
|
||||
[K in keyof PushNotificationDataSourceMap]: PushNotificationData<K>;
|
||||
};
|
||||
|
||||
export type badgeNames =
|
||||
'null'
|
||||
export type BadgeNames =
|
||||
| 'null'
|
||||
| 'antenna'
|
||||
| 'arrow-back-up'
|
||||
| 'at'
|
||||
| 'chart-arrows'
|
||||
| 'circle-check'
|
||||
| 'medal'
|
||||
| 'messages'
|
||||
| 'plus'
|
||||
| 'quote'
|
||||
| 'repeat'
|
||||
| 'user-plus'
|
||||
| 'users'
|
||||
;
|
||||
| 'users';
|
||||
|
|
432
pnpm-lock.yaml
432
pnpm-lock.yaml
|
@ -1014,8 +1014,8 @@ importers:
|
|||
packages/sw:
|
||||
dependencies:
|
||||
esbuild:
|
||||
specifier: 0.14.42
|
||||
version: 0.14.42
|
||||
specifier: 0.17.15
|
||||
version: 0.17.15
|
||||
idb-keyval:
|
||||
specifier: 6.2.0
|
||||
version: 6.2.0
|
||||
|
@ -3655,6 +3655,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm64@0.17.15:
|
||||
resolution: {integrity: sha512-0kOB6Y7Br3KDVgHeg8PRcvfLkq+AccreK///B4Z6fNZGr/tNHX0z2VywCc7PTeWp+bPvjA5WMvNXltHw5QjAIA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm@0.17.14:
|
||||
resolution: {integrity: sha512-0CnlwnjDU8cks0yJLXfkaU/uoLyRf9VZJs4p1PskBr2AlAHeEsFEwJEo0of/Z3g+ilw5mpyDwThlxzNEIxOE4g==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3663,6 +3671,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm@0.17.15:
|
||||
resolution: {integrity: sha512-sRSOVlLawAktpMvDyJIkdLI/c/kdRTOqo8t6ImVxg8yT7LQDUYV5Rp2FKeEosLr6ZCja9UjYAzyRSxGteSJPYg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-x64@0.17.14:
|
||||
resolution: {integrity: sha512-nrfQYWBfLGfSGLvRVlt6xi63B5IbfHm3tZCdu/82zuFPQ7zez4XjmRtF/wIRYbJQ/DsZrxJdEvYFE67avYXyng==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3671,6 +3687,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-x64@0.17.15:
|
||||
resolution: {integrity: sha512-MzDqnNajQZ63YkaUWVl9uuhcWyEyh69HGpMIrf+acR4otMkfLJ4sUCxqwbCyPGicE9dVlrysI3lMcDBjGiBBcQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-arm64@0.17.14:
|
||||
resolution: {integrity: sha512-eoSjEuDsU1ROwgBH/c+fZzuSyJUVXQTOIN9xuLs9dE/9HbV/A5IqdXHU1p2OfIMwBwOYJ9SFVGGldxeRCUJFyw==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3679,6 +3703,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-arm64@0.17.15:
|
||||
resolution: {integrity: sha512-7siLjBc88Z4+6qkMDxPT2juf2e8SJxmsbNVKFY2ifWCDT72v5YJz9arlvBw5oB4W/e61H1+HDB/jnu8nNg0rLA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-x64@0.17.14:
|
||||
resolution: {integrity: sha512-zN0U8RWfrDttdFNkHqFYZtOH8hdi22z0pFm0aIJPsNC4QQZv7je8DWCX5iA4Zx6tRhS0CCc0XC2m7wKsbWEo5g==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3687,6 +3719,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-x64@0.17.15:
|
||||
resolution: {integrity: sha512-NbImBas2rXwYI52BOKTW342Tm3LTeVlaOQ4QPZ7XuWNKiO226DisFk/RyPk3T0CKZkKMuU69yOvlapJEmax7cg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-arm64@0.17.14:
|
||||
resolution: {integrity: sha512-z0VcD4ibeZWVQCW1O7szaLxGsx54gcCnajEJMdYoYjLiq4g1jrP2lMq6pk71dbS5+7op/L2Aod+erw+EUr28/A==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3695,6 +3735,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-arm64@0.17.15:
|
||||
resolution: {integrity: sha512-Xk9xMDjBVG6CfgoqlVczHAdJnCs0/oeFOspFap5NkYAmRCT2qTn1vJWA2f419iMtsHSLm+O8B6SLV/HlY5cYKg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-x64@0.17.14:
|
||||
resolution: {integrity: sha512-hd9mPcxfTgJlolrPlcXkQk9BMwNBvNBsVaUe5eNUqXut6weDQH8whcNaKNF2RO8NbpT6GY8rHOK2A9y++s+ehw==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3703,6 +3751,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-x64@0.17.15:
|
||||
resolution: {integrity: sha512-3TWAnnEOdclvb2pnfsTWtdwthPfOz7qAfcwDLcfZyGJwm1SRZIMOeB5FODVhnM93mFSPsHB9b/PmxNNbSnd0RQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm64@0.17.14:
|
||||
resolution: {integrity: sha512-FhAMNYOq3Iblcj9i+K0l1Fp/MHt+zBeRu/Qkf0LtrcFu3T45jcwB6A1iMsemQ42vR3GBhjNZJZTaCe3VFPbn9g==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3711,6 +3767,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm64@0.17.15:
|
||||
resolution: {integrity: sha512-T0MVnYw9KT6b83/SqyznTs/3Jg2ODWrZfNccg11XjDehIved2oQfrX/wVuev9N936BpMRaTR9I1J0tdGgUgpJA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm@0.17.14:
|
||||
resolution: {integrity: sha512-BNTl+wSJ1omsH8s3TkQmIIIQHwvwJrU9u1ggb9XU2KTVM4TmthRIVyxSp2qxROJHhZuW/r8fht46/QE8hU8Qvg==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3719,6 +3783,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm@0.17.15:
|
||||
resolution: {integrity: sha512-MLTgiXWEMAMr8nmS9Gigx43zPRmEfeBfGCwxFQEMgJ5MC53QKajaclW6XDPjwJvhbebv+RzK05TQjvH3/aM4Xw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ia32@0.17.14:
|
||||
resolution: {integrity: sha512-91OK/lQ5y2v7AsmnFT+0EyxdPTNhov3y2CWMdizyMfxSxRqHazXdzgBKtlmkU2KYIc+9ZK3Vwp2KyXogEATYxQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3727,6 +3799,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ia32@0.17.15:
|
||||
resolution: {integrity: sha512-wp02sHs015T23zsQtU4Cj57WiteiuASHlD7rXjKUyAGYzlOKDAjqK6bk5dMi2QEl/KVOcsjwL36kD+WW7vJt8Q==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-loong64@0.17.14:
|
||||
resolution: {integrity: sha512-vp15H+5NR6hubNgMluqqKza85HcGJgq7t6rMH7O3Y6ApiOWPkvW2AJfNojUQimfTp6OUrACUXfR4hmpcENXoMQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3735,6 +3815,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-loong64@0.17.15:
|
||||
resolution: {integrity: sha512-k7FsUJjGGSxwnBmMh8d7IbObWu+sF/qbwc+xKZkBe/lTAF16RqxRCnNHA7QTd3oS2AfGBAnHlXL67shV5bBThQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-mips64el@0.17.14:
|
||||
resolution: {integrity: sha512-90TOdFV7N+fgi6c2+GO9ochEkmm9kBAKnuD5e08GQMgMINOdOFHuYLPQ91RYVrnWwQ5683sJKuLi9l4SsbJ7Hg==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3743,6 +3831,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-mips64el@0.17.15:
|
||||
resolution: {integrity: sha512-ZLWk6czDdog+Q9kE/Jfbilu24vEe/iW/Sj2d8EVsmiixQ1rM2RKH2n36qfxK4e8tVcaXkvuV3mU5zTZviE+NVQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ppc64@0.17.14:
|
||||
resolution: {integrity: sha512-NnBGeoqKkTugpBOBZZoktQQ1Yqb7aHKmHxsw43NddPB2YWLAlpb7THZIzsRsTr0Xw3nqiPxbA1H31ZMOG+VVPQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3751,6 +3847,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ppc64@0.17.15:
|
||||
resolution: {integrity: sha512-mY6dPkIRAiFHRsGfOYZC8Q9rmr8vOBZBme0/j15zFUKM99d4ILY4WpOC7i/LqoY+RE7KaMaSfvY8CqjJtuO4xg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-riscv64@0.17.14:
|
||||
resolution: {integrity: sha512-0qdlKScLXA8MGVy21JUKvMzCYWovctuP8KKqhtE5A6IVPq4onxXhSuhwDd2g5sRCzNDlDjitc5sX31BzDoL5Fw==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3759,6 +3863,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-riscv64@0.17.15:
|
||||
resolution: {integrity: sha512-EcyUtxffdDtWjjwIH8sKzpDRLcVtqANooMNASO59y+xmqqRYBBM7xVLQhqF7nksIbm2yHABptoioS9RAbVMWVA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-s390x@0.17.14:
|
||||
resolution: {integrity: sha512-Hdm2Jo1yaaOro4v3+6/zJk6ygCqIZuSDJHdHaf8nVH/tfOuoEX5Riv03Ka15LmQBYJObUTNS1UdyoMk0WUn9Ww==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3767,6 +3879,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-s390x@0.17.15:
|
||||
resolution: {integrity: sha512-BuS6Jx/ezxFuHxgsfvz7T4g4YlVrmCmg7UAwboeyNNg0OzNzKsIZXpr3Sb/ZREDXWgt48RO4UQRDBxJN3B9Rbg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-x64@0.17.14:
|
||||
resolution: {integrity: sha512-8KHF17OstlK4DuzeF/KmSgzrTWQrkWj5boluiiq7kvJCiQVzUrmSkaBvcLB2UgHpKENO2i6BthPkmUhNDaJsVw==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3775,6 +3895,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-x64@0.17.15:
|
||||
resolution: {integrity: sha512-JsdS0EgEViwuKsw5tiJQo9UdQdUJYuB+Mf6HxtJSPN35vez1hlrNb1KajvKWF5Sa35j17+rW1ECEO9iNrIXbNg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/netbsd-x64@0.17.14:
|
||||
resolution: {integrity: sha512-nVwpqvb3yyXztxIT2+VsxJhB5GCgzPdk1n0HHSnchRAcxqKO6ghXwHhJnr0j/B+5FSyEqSxF4q03rbA2fKXtUQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3783,6 +3911,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/netbsd-x64@0.17.15:
|
||||
resolution: {integrity: sha512-R6fKjtUysYGym6uXf6qyNephVUQAGtf3n2RCsOST/neIwPqRWcnc3ogcielOd6pT+J0RDR1RGcy0ZY7d3uHVLA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/openbsd-x64@0.17.14:
|
||||
resolution: {integrity: sha512-1RZ7uQQ9zcy/GSAJL1xPdN7NDdOOtNEGiJalg/MOzeakZeTrgH/DoCkbq7TaPDiPhWqnDF+4bnydxRqQD7il6g==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3791,6 +3927,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/openbsd-x64@0.17.15:
|
||||
resolution: {integrity: sha512-mVD4PGc26b8PI60QaPUltYKeSX0wxuy0AltC+WCTFwvKCq2+OgLP4+fFd+hZXzO2xW1HPKcytZBdjqL6FQFa7w==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/sunos-x64@0.17.14:
|
||||
resolution: {integrity: sha512-nqMjDsFwv7vp7msrwWRysnM38Sd44PKmW8EzV01YzDBTcTWUpczQg6mGao9VLicXSgW/iookNK6AxeogNVNDZA==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3799,6 +3943,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/sunos-x64@0.17.15:
|
||||
resolution: {integrity: sha512-U6tYPovOkw3459t2CBwGcFYfFRjivcJJc1WC8Q3funIwX8x4fP+R6xL/QuTPNGOblbq/EUDxj9GU+dWKX0oWlQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-arm64@0.17.14:
|
||||
resolution: {integrity: sha512-xrD0mccTKRBBIotrITV7WVQAwNJ5+1va6L0H9zN92v2yEdjfAN7864cUaZwJS7JPEs53bDTzKFbfqVlG2HhyKQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3807,6 +3959,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-arm64@0.17.15:
|
||||
resolution: {integrity: sha512-W+Z5F++wgKAleDABemiyXVnzXgvRFs+GVKThSI+mGgleLWluv0D7Diz4oQpgdpNzh4i2nNDzQtWbjJiqutRp6Q==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-ia32@0.17.14:
|
||||
resolution: {integrity: sha512-nXpkz9bbJrLLyUTYtRotSS3t5b+FOuljg8LgLdINWFs3FfqZMtbnBCZFUmBzQPyxqU87F8Av+3Nco/M3hEcu1w==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3815,6 +3975,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-ia32@0.17.15:
|
||||
resolution: {integrity: sha512-Muz/+uGgheShKGqSVS1KsHtCyEzcdOn/W/Xbh6H91Etm+wiIfwZaBn1W58MeGtfI8WA961YMHFYTthBdQs4t+w==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-x64@0.17.14:
|
||||
resolution: {integrity: sha512-gPQmsi2DKTaEgG14hc3CHXHp62k8g6qr0Pas+I4lUxRMugGSATh/Bi8Dgusoz9IQ0IfdrvLpco6kujEIBoaogA==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3823,6 +3991,14 @@ packages:
|
|||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-x64@0.17.15:
|
||||
resolution: {integrity: sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@eslint-community/eslint-utils@4.4.0(eslint@8.37.0):
|
||||
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
@ -5284,10 +5460,10 @@ packages:
|
|||
'@storybook/node-logger': 7.0.2
|
||||
'@types/ejs': 3.1.2
|
||||
'@types/find-cache-dir': 3.2.1
|
||||
'@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.17.14)
|
||||
'@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.17.15)
|
||||
browser-assert: 1.2.1
|
||||
ejs: 3.1.8
|
||||
esbuild: 0.17.14
|
||||
esbuild: 0.17.15
|
||||
esbuild-plugin-alias: 0.2.1
|
||||
express: 4.18.2
|
||||
find-cache-dir: 3.3.2
|
||||
|
@ -5495,8 +5671,8 @@ packages:
|
|||
'@types/node': 16.18.16
|
||||
'@types/pretty-hrtime': 1.0.1
|
||||
chalk: 4.1.2
|
||||
esbuild: 0.17.14
|
||||
esbuild-register: 3.4.2(esbuild@0.17.14)
|
||||
esbuild: 0.17.15
|
||||
esbuild-register: 3.4.2(esbuild@0.17.15)
|
||||
file-system-cache: 2.0.2
|
||||
find-up: 5.0.0
|
||||
fs-extra: 11.1.0
|
||||
|
@ -7403,13 +7579,13 @@ packages:
|
|||
tunnel: 0.0.6
|
||||
dev: true
|
||||
|
||||
/@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15(esbuild@0.17.14):
|
||||
/@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15(esbuild@0.17.15):
|
||||
resolution: {integrity: sha512-kYzDJO5CA9sy+on/s2aIW0411AklfCi8Ck/4QDivOqsMKpStZA2SsR+X27VTggGwpStWaLrjJcDcdDMowtG8MA==}
|
||||
engines: {node: '>=14.15.0'}
|
||||
peerDependencies:
|
||||
esbuild: '>=0.10.0'
|
||||
dependencies:
|
||||
esbuild: 0.17.14
|
||||
esbuild: 0.17.15
|
||||
tslib: 2.5.0
|
||||
dev: true
|
||||
|
||||
|
@ -10330,228 +10506,21 @@ packages:
|
|||
es6-symbol: 3.1.3
|
||||
dev: false
|
||||
|
||||
/esbuild-android-64@0.14.42:
|
||||
resolution: {integrity: sha512-P4Y36VUtRhK/zivqGVMqhptSrFILAGlYp0Z8r9UQqHJ3iWztRCNWnlBzD9HRx0DbueXikzOiwyOri+ojAFfW6A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-android-arm64@0.14.42:
|
||||
resolution: {integrity: sha512-0cOqCubq+RWScPqvtQdjXG3Czb3AWI2CaKw3HeXry2eoA2rrPr85HF7IpdU26UWdBXgPYtlTN1LUiuXbboROhg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-darwin-64@0.14.42:
|
||||
resolution: {integrity: sha512-ipiBdCA3ZjYgRfRLdQwP82rTiv/YVMtW36hTvAN5ZKAIfxBOyPXY7Cejp3bMXWgzKD8B6O+zoMzh01GZsCuEIA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-darwin-arm64@0.14.42:
|
||||
resolution: {integrity: sha512-bU2tHRqTPOaoH/4m0zYHbFWpiYDmaA0gt90/3BMEFaM0PqVK/a6MA2V/ypV5PO0v8QxN6gH5hBPY4YJ2lopXgA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-freebsd-64@0.14.42:
|
||||
resolution: {integrity: sha512-75h1+22Ivy07+QvxHyhVqOdekupiTZVLN1PMwCDonAqyXd8TVNJfIRFrdL8QmSJrOJJ5h8H1I9ETyl2L8LQDaw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-freebsd-arm64@0.14.42:
|
||||
resolution: {integrity: sha512-W6Jebeu5TTDQMJUJVarEzRU9LlKpNkPBbjqSu+GUPTHDCly5zZEQq9uHkmHHl7OKm+mQ2zFySN83nmfCeZCyNA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-32@0.14.42:
|
||||
resolution: {integrity: sha512-Ooy/Bj+mJ1z4jlWcK5Dl6SlPlCgQB9zg1UrTCeY8XagvuWZ4qGPyYEWGkT94HUsRi2hKsXvcs6ThTOjBaJSMfg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-64@0.14.42:
|
||||
resolution: {integrity: sha512-2L0HbzQfbTuemUWfVqNIjOfaTRt9zsvjnme6lnr7/MO9toz/MJ5tZhjqrG6uDWDxhsaHI2/nsDgrv8uEEN2eoA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-arm64@0.14.42:
|
||||
resolution: {integrity: sha512-c3Ug3e9JpVr8jAcfbhirtpBauLxzYPpycjWulD71CF6ZSY26tvzmXMJYooQ2YKqDY4e/fPu5K8bm7MiXMnyxuA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-arm@0.14.42:
|
||||
resolution: {integrity: sha512-STq69yzCMhdRaWnh29UYrLSr/qaWMm/KqwaRF1pMEK7kDiagaXhSL1zQGXbYv94GuGY/zAwzK98+6idCMUOOCg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-mips64le@0.14.42:
|
||||
resolution: {integrity: sha512-QuvpHGbYlkyXWf2cGm51LBCHx6eUakjaSrRpUqhPwjh/uvNUYvLmz2LgPTTPwCqaKt0iwL+OGVL0tXA5aDbAbg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-ppc64le@0.14.42:
|
||||
resolution: {integrity: sha512-8ohIVIWDbDT+i7lCx44YCyIRrOW1MYlks9fxTo0ME2LS/fxxdoJBwHWzaDYhjvf8kNpA+MInZvyOEAGoVDrMHg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-riscv64@0.14.42:
|
||||
resolution: {integrity: sha512-DzDqK3TuoXktPyG1Lwx7vhaF49Onv3eR61KwQyxYo4y5UKTpL3NmuarHSIaSVlTFDDpcIajCDwz5/uwKLLgKiQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-s390x@0.14.42:
|
||||
resolution: {integrity: sha512-YFRhPCxl8nb//Wn6SiS5pmtplBi4z9yC2gLrYoYI/tvwuB1jldir9r7JwAGy1Ck4D7sE7wBN9GFtUUX/DLdcEQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-netbsd-64@0.14.42:
|
||||
resolution: {integrity: sha512-QYSD2k+oT9dqB/4eEM9c+7KyNYsIPgzYOSrmfNGDIyJrbT1d+CFVKvnKahDKNJLfOYj8N4MgyFaU9/Ytc6w5Vw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-openbsd-64@0.14.42:
|
||||
resolution: {integrity: sha512-M2meNVIKWsm2HMY7+TU9AxM7ZVwI9havdsw6m/6EzdXysyCFFSoaTQ/Jg03izjCsK17FsVRHqRe26Llj6x0MNA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-plugin-alias@0.2.1:
|
||||
resolution: {integrity: sha512-jyfL/pwPqaFXyKnj8lP8iLk6Z0m099uXR45aSN8Av1XD4vhvQutxxPzgA2bTcAwQpa1zCXDcWOlhFgyP3GKqhQ==}
|
||||
dev: true
|
||||
|
||||
/esbuild-register@3.4.2(esbuild@0.17.14):
|
||||
/esbuild-register@3.4.2(esbuild@0.17.15):
|
||||
resolution: {integrity: sha512-kG/XyTDyz6+YDuyfB9ZoSIOOmgyFCH+xPRtsCa8W85HLRV5Csp+o3jWVbOSHgSLfyLc5DmP+KFDNwty4mEjC+Q==}
|
||||
peerDependencies:
|
||||
esbuild: '>=0.12 <1'
|
||||
dependencies:
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
esbuild: 0.17.14
|
||||
esbuild: 0.17.15
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/esbuild-sunos-64@0.14.42:
|
||||
resolution: {integrity: sha512-uXV8TAZEw36DkgW8Ak3MpSJs1ofBb3Smkc/6pZ29sCAN1KzCAQzsje4sUwugf+FVicrHvlamCOlFZIXgct+iqQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-windows-32@0.14.42:
|
||||
resolution: {integrity: sha512-4iw/8qWmRICWi9ZOnJJf9sYt6wmtp3hsN4TdI5NqgjfOkBVMxNdM9Vt3626G1Rda9ya2Q0hjQRD9W1o+m6Lz6g==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-windows-64@0.14.42:
|
||||
resolution: {integrity: sha512-j3cdK+Y3+a5H0wHKmLGTJcq0+/2mMBHPWkItR3vytp/aUGD/ua/t2BLdfBIzbNN9nLCRL9sywCRpOpFMx3CxzA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-windows-arm64@0.14.42:
|
||||
resolution: {integrity: sha512-+lRAARnF+hf8J0mN27ujO+VbhPbDqJ8rCcJKye4y7YZLV6C4n3pTRThAb388k/zqF5uM0lS5O201u0OqoWSicw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild@0.14.42:
|
||||
resolution: {integrity: sha512-V0uPZotCEHokJdNqyozH6qsaQXqmZEOiZWrXnds/zaH/0SyrIayRXWRB98CENO73MIZ9T3HBIOsmds5twWtmgw==}
|
||||
engines: {node: '>=12'}
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
esbuild-android-64: 0.14.42
|
||||
esbuild-android-arm64: 0.14.42
|
||||
esbuild-darwin-64: 0.14.42
|
||||
esbuild-darwin-arm64: 0.14.42
|
||||
esbuild-freebsd-64: 0.14.42
|
||||
esbuild-freebsd-arm64: 0.14.42
|
||||
esbuild-linux-32: 0.14.42
|
||||
esbuild-linux-64: 0.14.42
|
||||
esbuild-linux-arm: 0.14.42
|
||||
esbuild-linux-arm64: 0.14.42
|
||||
esbuild-linux-mips64le: 0.14.42
|
||||
esbuild-linux-ppc64le: 0.14.42
|
||||
esbuild-linux-riscv64: 0.14.42
|
||||
esbuild-linux-s390x: 0.14.42
|
||||
esbuild-netbsd-64: 0.14.42
|
||||
esbuild-openbsd-64: 0.14.42
|
||||
esbuild-sunos-64: 0.14.42
|
||||
esbuild-windows-32: 0.14.42
|
||||
esbuild-windows-64: 0.14.42
|
||||
esbuild-windows-arm64: 0.14.42
|
||||
dev: false
|
||||
|
||||
/esbuild@0.17.14:
|
||||
resolution: {integrity: sha512-vOO5XhmVj/1XQR9NQ1UPq6qvMYL7QFJU57J5fKBKBKxp17uDt5PgxFDb4A2nEiXhr1qQs4x0F5+66hVVw4ruNw==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -10581,6 +10550,35 @@ packages:
|
|||
'@esbuild/win32-ia32': 0.17.14
|
||||
'@esbuild/win32-x64': 0.17.14
|
||||
|
||||
/esbuild@0.17.15:
|
||||
resolution: {integrity: sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@esbuild/android-arm': 0.17.15
|
||||
'@esbuild/android-arm64': 0.17.15
|
||||
'@esbuild/android-x64': 0.17.15
|
||||
'@esbuild/darwin-arm64': 0.17.15
|
||||
'@esbuild/darwin-x64': 0.17.15
|
||||
'@esbuild/freebsd-arm64': 0.17.15
|
||||
'@esbuild/freebsd-x64': 0.17.15
|
||||
'@esbuild/linux-arm': 0.17.15
|
||||
'@esbuild/linux-arm64': 0.17.15
|
||||
'@esbuild/linux-ia32': 0.17.15
|
||||
'@esbuild/linux-loong64': 0.17.15
|
||||
'@esbuild/linux-mips64el': 0.17.15
|
||||
'@esbuild/linux-ppc64': 0.17.15
|
||||
'@esbuild/linux-riscv64': 0.17.15
|
||||
'@esbuild/linux-s390x': 0.17.15
|
||||
'@esbuild/linux-x64': 0.17.15
|
||||
'@esbuild/netbsd-x64': 0.17.15
|
||||
'@esbuild/openbsd-x64': 0.17.15
|
||||
'@esbuild/sunos-x64': 0.17.15
|
||||
'@esbuild/win32-arm64': 0.17.15
|
||||
'@esbuild/win32-ia32': 0.17.15
|
||||
'@esbuild/win32-x64': 0.17.15
|
||||
|
||||
/escalade@3.1.1:
|
||||
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
Loading…
Reference in New Issue