diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 940b095fe29c511aeb5409ba2d6e9610c508d7b7..29217462958ca496490975d758049d9edf745bf1 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -95,6 +95,14 @@ redis: # #prefix: example-prefix # #db: 1 +#redisForTimelines: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + # ┌───────────────────────────┠#───┘ MeiliSearch configuration └───────────────────────────── diff --git a/.config/example.yml b/.config/example.yml index 03864a32994f04662d588951856c783f2ec2848d..0e4f2f5a15cdb5ec41da75382c7e0d2d985b48ca 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -105,6 +105,16 @@ redis: # # You can specify more ioredis options... # #username: example-username +#redisForTimelines: +# host: localhost +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 +# # You can specify more ioredis options... +# #username: example-username + # ┌───────────────────────────┠#───┘ MeiliSearch configuration └───────────────────────────── diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 861b0008a047dff9cff035a688b0917a3855595e..a78d91900bedceecf9df984faa3a40a349a86b93 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,9 @@ "service": "app", "workspaceFolder": "/workspace", "features": { - "ghcr.io/devcontainers-contrib/features/pnpm:2": {}, + "ghcr.io/devcontainers-contrib/features/pnpm:2": { + "version": "8.8.0" + }, "ghcr.io/devcontainers/features/node:1": { "version": "20.5.1" } diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index 5dcd41599acd8ba92524a36c6bb5ce0a968ffe2d..3d57d1245daa5141ea62a533505d648d57338c27 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -95,6 +95,14 @@ redis: # #prefix: example-prefix # #db: 1 +#redisForTimelines: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + # ┌───────────────────────────┠#───┘ MeiliSearch configuration └───────────────────────────── diff --git a/CHANGELOG.md b/CHANGELOG.md index ee2f11b1564025f64d319a88c5ebb5843e577bc5..d8714599f251b0902fd6055303ca3f7f497115ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,53 @@ --> +## 2023.10.0 +### NOTE +- 2023.9.2ã§å°Žå…¥ã•ã‚ŒãŸãƒŽãƒ¼ãƒˆç·¨é›†æ©Ÿèƒ½ã¯ã‚¯ã‚ªãƒªãƒ†ã‚£ã®é«˜ã„実装ãŒå›°é›£ã§ã‚ã‚‹ã“ã¨ãŒåˆ¤æ˜Žã—ãŸãŸã‚撤回ã•ã‚Œã¾ã—㟠+- アップデートを行ã†ã¨ã€ã‚¿ã‚¤ãƒ ラインãŒä¸€æ™‚çš„ã«ãƒªã‚»ãƒƒãƒˆã•ã‚Œã¾ã™ + +### Changes +- API: users/notes, notes/local-timeline 㧠fileType 指定ã¯ã§ããªããªã‚Šã¾ã—㟠+- API: notes/featured ã§ãƒšãƒ¼ã‚¸ãƒãƒ¼ã‚·ãƒ§ãƒ³ã¯ä»–APIã¨åŒæ§˜ untilId を使ã£ã¦è¡Œã†ã‚ˆã†ã«ãªã‚Šã¾ã—㟠+ +### General +- Feat: ユーザーã”ã¨ã«ä»–ユーザーã¸ã®è¿”信をタイムラインã«å«ã‚ã‚‹ã‹è¨å®šå¯èƒ½ã«ãªã‚Šã¾ã—㟠+- Feat: ユーザーリスト内ã®ãƒ¡ãƒ³ãƒãƒ¼ã”ã¨ã«ä»–ユーザーã¸ã®è¿”信をユーザーリストタイムラインã«å«ã‚ã‚‹ã‹è¨å®šå¯èƒ½ã«ãªã‚Šã¾ã—㟠+- Feat: ユーザーã”ã¨ã®ãƒã‚¤ãƒ©ã‚¤ãƒˆ +- Feat: プライãƒã‚·ãƒ¼ãƒãƒªã‚·ãƒ¼ãƒ»é‹å–¶è€…æƒ…å ±ï¼ˆImpressum)ã®æŒ‡å®šãŒå¯èƒ½ã«ãªã‚Šã¾ã—㟠+ - プライãƒã‚·ãƒ¼ãƒãƒªã‚·ãƒ¼ã¯ã‚µãƒ¼ãƒãƒ¼ç™»éŒ²æ™‚ã«åŒæ„確èªãŒå…¥ã‚Šã¾ã™ +- Feat: タイムラインãŒãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ æ›´æ–°ä¸ã«åºƒå‘Šã‚’挿入ã§ãるよã†ã«ãªã‚Šã¾ã—㟠+ - デフォルトã¯ç„¡åŠ¹ + - é »åº¦ã¯ã‚³ãƒ³ãƒˆãƒãƒ¼ãƒ«ãƒ‘ãƒãƒ«ã‹ã‚‰è¨å®šã§ãã¾ã™ã€‚é‹å–¶ä¸ã®ã‚µãƒ¼ãƒãƒ¼ã®TLã®æµé€Ÿã‚’見ã¦ã€æœ€é©ãªå€¤ã‚’指定ã—ã¦ãã ã•ã„。 +- Enhance: ソフトワードミュートã¨ãƒãƒ¼ãƒ‰ãƒ¯ãƒ¼ãƒ‰ãƒŸãƒ¥ãƒ¼ãƒˆã¯çµ±åˆã•ã‚Œã¾ã—㟠+- Enhance: モデレーションãƒã‚°æ©Ÿèƒ½ã®å¼·åŒ– +- Enhance: ãƒãƒ¼ã‚«ãƒªã‚¼ãƒ¼ã‚·ãƒ§ãƒ³ã®æ›´æ–° +- Enhance: ä¾å˜é–¢ä¿‚ã®æ›´æ–° +- Fix: ダイレクト投稿をリノートã§ãã¦ã—ã¾ã†å•é¡Œã‚’ä¿®æ£ +- Fix: ユーザーリストTLã«ãƒãƒ£ãƒ³ãƒãƒ«æŠ•ç¨¿ãŒå«ã¾ã‚Œã‚‹å•é¡Œã‚’ä¿®æ£ + +### Client +- Feat: 「ファイルã®è©³ç´°ã€ãƒšãƒ¼ã‚¸ã‚’è¿½åŠ + - ドライブã®ãƒ•ã‚¡ã‚¤ãƒ«ã®æ‹¡å¤§ãƒ—レビューãŒã§ãるよã†ã« + - ファイルãŒæ·»ä»˜ã•ã‚ŒãŸãƒŽãƒ¼ãƒˆã®ä¸€è¦§ãŒè¡¨ç¤ºã§ãるよã†ã« +- Enhance: 二è¦ç´ èªè¨¼ã®ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—コード一覧をテã‚ストファイルã§ãƒ€ã‚¦ãƒ³ãƒãƒ¼ãƒ‰å¯èƒ½ã« +- Enhance: å‹•ç”»å†ç”Ÿæ™‚ã®ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆãƒœãƒªãƒ¥ãƒ¼ãƒ ã‚’30%ã« +- Fix: リアクションã—ãŸãƒ¦ãƒ¼ã‚¶ä¸€è¦§ã®UIãŒç¨€ã«å·¦ä¸Šã«æ®‹ã£ã¦ã—ã¾ã†ä¸å…·åˆã‚’ä¿®æ£ + +### Server +- Enhance: drive/files/attached-notes ãŒãƒšãƒ¼ã‚¸ãƒãƒ¼ã‚·ãƒ§ãƒ³ã«å¯¾å¿œã—ã¾ã—㟠+- Enhance: タイムラインå–得時ã®ãƒ‘フォーマンスを大幅ã«å‘上 +- Enhance: ãƒã‚¤ãƒ©ã‚¤ãƒˆå–得時ã®ãƒ‘フォーマンスを大幅ã«å‘上 +- Enhance: トレンドãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°å–得時ã®ãƒ‘フォーマンスを大幅ã«å‘上 +- Enhance: WebSocket接続ãŒå¤šã„å ´åˆã®ãƒ‘フォーマンスをå‘上 +- Enhance: ä¸è¦ãªPostgreSQLã®ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ã‚’削除ã—パフォーマンスをå‘上 +- Fix: 連åˆãªã—アンケートã«æŠ•ç¥¨ã‚’ã™ã‚‹ã¨UpdateãŒãƒªãƒ¢ãƒ¼ãƒˆã«é…ä¿¡ã•ã‚Œã¦ã—ã¾ã†ã®ã‚’ä¿®æ£ +- Fix: nodeinfoã«ãŠã„ã¦CORS用ã®ãƒ˜ãƒƒãƒ€ãƒ¼ãŒè¨å®šã•ã‚Œã¦ã„ãªã„ã®ã‚’ä¿®æ£ +- Fix: åŒã˜ç¨®é¡žã®TLã®ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã‚’複数接続ã§ããªã„å•é¡Œã‚’ä¿®æ£ +- Fix: アンテナTLを途ä¸ã¾ã§ã—ã‹ãƒšãƒ¼ã‚¸ãƒãƒ¼ã‚·ãƒ§ãƒ³ã§ããªããªã‚‹ã“ã¨ãŒã‚ã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: 「ファイル付ãã®ã¿ã€ã®TLã§ãƒ•ã‚¡ã‚¤ãƒ«ç„¡ã—ã®æ–°ç€ãƒŽãƒ¼ãƒˆãŒæµã‚Œã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: プãƒã‚»ã‚¹ãŒçµ‚了ã—ãªã„ã€ã‚ã‚‹ã„ã¯éžå¸¸ã«æ™‚é–“ãŒã‹ã‹ã‚‹å•é¡Œã‚’ä¿®æ£ + ## 2023.9.3 ### General - Enhance: ノートã®ç¿»è¨³æ©Ÿèƒ½ã®åˆ©ç”¨å¯å¦ã‚’ãƒãƒ¼ãƒ«ã§è¨å®šå¯èƒ½ã« diff --git a/chart/files/default.yml b/chart/files/default.yml index 90b574b99f400eb44aed4f4abc9c4347273e2911..87b2f677ebe2ac0622b549a396ebcc1696a729c3 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -116,6 +116,14 @@ redis: # #prefix: example-prefix # #db: 1 +#redisForTimelines: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + # ┌───────────────────────────┠#───┘ MeiliSearch configuration └───────────────────────────── diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 55b7cbb88c9babb521b63951deaf34797933b4c1..e835c4aeeeb6b7124fc21fc493eb24b405e6d0f3 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1184,11 +1184,6 @@ _wordMute: muteWords: "الكلمات المØظورة" muteWordsDescription: "اÙصل بينهم بمساÙØ© لاستخدام معامل \"Ùˆ\" أو بسطر لاستخدام معامل \"أو\"." muteWordsDescription2: "اØصر الكلمات المÙتاØية بين بين شرطتين مائلتين لاستخدامها كتعابير نمطية" - softDescription: "اخ٠الملاØظات التي تستو٠الشروط من الخيط الزمني." - hardDescription: "اخ٠الملاØظات التي تستو٠الشروط من الخيط الزمني.بالإضاÙØ© إلى أن هذه الملاØظات ستبقى مخÙية Øتى وإن تغيرت الشروط." - soft: "لينة" - hard: "قاسية" - mutedNotes: "الملاØظات المكتومة" _instanceMute: instanceMuteDescription: "هذه سيØجب كل ملاØظات الخوادم المØجوبة ومشاركاتها والردود على تلك الملاØظات Øتى وإن كانت من خادم غير Ù…Øجوب." instanceMuteDescription2: "مدخلة لكل سطر" @@ -1248,8 +1243,6 @@ _sfx: note: "الملاØظات" noteMy: "ملاØظتي" notification: "الإشعارات" - chat: "المØادثة" - chatBg: "المØادثة (الخلÙية)" antenna: "الهوائيات" channel: "إشعارات القنات" _ago: diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 64b32d176b2f107cb4330a5a5803ea36f1043834..4baa3d672e7b3ef0309b82ad01d874611020a84a 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -932,11 +932,6 @@ _wordMute: muteWords: "নিঃশবà§à¦¦ করা শবà§à¦¦à¦—à§à¦²à¦¿" muteWordsDescription: "সà§à¦ªà§‡à¦¸ দিয়ে আলাদা করলে AND শরà§à¦¤ তৈরি হবে à¦à¦¬à¦‚ আলাদা লাইনে লিখলে OR শরà§à¦¤ তৈরি হবে।" muteWordsDescription2: "রেগà§à¦²à¦¾à¦° à¦à¦•à§à¦¸à¦ªà§à¦°à§‡à¦¶à¦¨ বà§à¦¯à¦¬à¦¹à¦¾à¦° করতে সà§à¦²à§à¦¯à¦¾à¦¶ দিয়ে কীওয়ারà§à¦¡à¦•à§‡ ঘিরে রাখà§à¦¨à¥¤" - softDescription: "টাইমলাইন থেকে নিরà§à¦¦à¦¿à¦·à§à¦Ÿ শরà§à¦¤à¦¾à¦¨à§à¦¯à¦¾à§Ÿà§€ নোট লà§à¦•à¦¿à¦¯à¦¼à§‡ রাখে।" - hardDescription: "নিরà§à¦¦à¦¿à¦·à§à¦Ÿ শরà§à¦¤à¦¾à¦¨à§à¦¯à¦¾à§Ÿà§€ নোটগà§à¦²à¦¿à¦•à§‡ টাইমলাইন থেকে বাদ দেয়। আপনি শরà§à¦¤ পরিবরà§à¦¤à¦¨ করলেও যে নোটগà§à¦²à¦¿ যোগ করা হয়নি সেগà§à¦²à¦¿ বাদ দেওয়া হবে।" - soft: "নমনীয়" - hard: "কঠোর" - mutedNotes: "মিউট করা নোটগà§à¦²à¦¿" _instanceMute: instanceMuteDescription: "কনফিগার করা ইনà§à¦¸à¦Ÿà§à¦¯à¦¾à¦¨à§à¦¸à§‡à¦° সব নোট à¦à¦¬à¦‚ রিনোট মিউট করà§à¦¨, মিউট করা ইনà§à¦¸à¦Ÿà§à¦¯à¦¾à¦¨à§à¦¸à§‡à¦° বà§à¦¯à¦¬à¦¹à¦¾à¦°à¦•à¦¾à¦°à§€à¦¦à§‡à¦° উতà§à¦¤à¦° সহ।" instanceMuteDescription2: "পà§à¦°à¦¤à¦¿à¦Ÿà¦¿à¦•à§‡ আলাদা লাইনে লিখà§à¦¨" @@ -1000,9 +995,6 @@ _theme: infoFg: "তথà§à¦¯à§‡à¦° পাঠà§à¦¯" infoWarnBg: "ওয়ারà§à¦¨à¦¿à¦‚ à¦à¦° পটà¦à§‚মি" infoWarnFg: "ওয়ারà§à¦¨à¦¿à¦‚ à¦à¦° পাঠà§à¦¯" - cwBg: "CW বাটনের পটà¦à§‚মি" - cwFg: "CW বাটনের পাঠà§à¦¯" - cwHoverBg: "CW বাটনের পটà¦à§‚মি (হà¦à¦¾à¦°)" toastBg: "বিজà§à¦žà¦ªà§à¦¤à¦¿à¦° পটà¦à§‚মি" toastFg: "বিজà§à¦žà¦ªà§à¦¤à¦¿à¦° পাঠà§à¦¯" buttonBg: "বাটনের পটà¦à§‚মি" @@ -1020,8 +1012,6 @@ _sfx: note: "নোটগà§à¦²à¦¿" noteMy: "নোট (আপনার)" notification: "বিজà§à¦žà¦ªà§à¦¤à¦¿" - chat: "চà§à¦¯à¦¾à¦Ÿ" - chatBg: "চà§à¦¯à¦¾à¦Ÿ (বà§à¦¯à¦¾à¦•à¦—à§à¦°à¦¾à¦‰à¦¨à§à¦¡)" antenna: "অà§à¦¯à¦¾à¦¨à§à¦Ÿà§‡à¦¨à¦¾à¦—à§à¦²à¦¿" channel: "চà§à¦¯à¦¾à¦¨à§‡à¦²à§‡à¦° বিজà§à¦žà¦ªà§à¦¤à¦¿" _ago: diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index d1fd73b666c93e4cdae9bbfd0f983a6c78fb8d84..915388006fa67ddaf66a76bb418e79b1c05c8fda 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -398,7 +398,6 @@ _theme: _sfx: note: "Notes" notification: "Notificacions" - chat: "Xat" antenna: "Antenes" _2fa: renewTOTPCancel: "No, grà cies" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 762f033b13d7d3a43efc5762edf5bd880471e4b8..6bd21de93a9fc692ee3fdc66195d82bf3df7e9cb 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -1559,11 +1559,6 @@ _wordMute: muteWords: "Ztlumená slova" muteWordsDescription: "PodmÃnku AND oddÄ›lujte mezerami, podmÃnku OR oddÄ›lujte řádkovými zlomy." muteWordsDescription2: "Chcete-li použÃt regulárnà výrazy, obklopte klÃÄová slova lomÃtky." - softDescription: "Skrýt poznámky, které splňujà nastavené podmÃnky, z Äasové osy." - hardDescription: "Zabránà pÅ™idánà poznámek splňujÃcÃch nastavené podmÃnky na Äasovou osu. KromÄ› toho nebudou tyto poznámky pÅ™idány na Äasovou osu, ani když se podmÃnky zmÄ›nÃ." - soft: "MÄ›kký" - hard: "Tvrdý" - mutedNotes: "Ztlumené poznámky" _instanceMute: instanceMuteDescription: "TÃmhle se ztlumà vÅ¡echny poznámky/poznámky z uvedených instancÃ, vÄetnÄ› poznámek uživatelů, kteřà odpovÃdajà uživateli ze ztlumené instance." instanceMuteDescription2: "OddÄ›lte novými řádky" @@ -1627,9 +1622,6 @@ _theme: infoFg: "Text informacÃ" infoWarnBg: "Pozadà varovánÃ" infoWarnFg: "Text varovánÃ" - cwBg: "Pozadà CW tlaÄÃtka" - cwFg: "Text CW tlaÄÃtka" - cwHoverBg: "Pozadà CW tlaÄÃtka (Hover)" toastBg: "Pozadà oznámenÃ" toastFg: "Text oznámenÃ" buttonBg: "Pozadà tlaÄÃtka" @@ -1647,8 +1639,6 @@ _sfx: note: "Poznámky" noteMy: "Moje poznámka" notification: "OznámenÃ" - chat: "Zprávy" - chatBg: "Chat (PozadÃ)" antenna: "Antény" channel: "Oznámenà kanálu" _ago: diff --git a/locales/de-DE.yml b/locales/de-DE.yml index e7d435a2e621f21c367b3dd808e91a534f8bef32..e4725ca7235ea3be292b13aaf11fbe710b58623a 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1126,6 +1126,15 @@ edited: "Bearbeitet" notificationRecieveConfig: "Benachrichtigungseinstellungen" mutualFollow: "Gegenseitig gefolgt" fileAttachedOnly: "Nur Notizen mit Dateien" +showRepliesToOthersInTimeline: "Antworten in Chronik anzeigen" +hideRepliesToOthersInTimeline: "Antworten nicht in Chronik anzeigen" +externalServices: "Externe Dienste" +impressum: "Impressum" +impressumUrl: "Impressums-URL" +impressumDescription: "In manchen Ländern, wie Deutschland und dessen Umgebung, ist die Angabe von Betreiberinformationen (ein Impressum) bei kommerziellem Betrieb zwingend." +privacyPolicy: "Datenschutzerklärung" +privacyPolicyUrl: "Datenschutzerklärungs-URL" +tosAndPrivacyPolicy: "Nutzungsbedingungen und Datenschutzerklärung" _announcement: forExistingUsers: "Nur für existierende Nutzer" forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt." @@ -1456,7 +1465,6 @@ _role: gtlAvailable: "Kann auf die globale Chronik zugreifen" ltlAvailable: "Kann auf die lokale Chronik zugreifen" canPublicNote: "Kann öffentliche Notizen erstellen" - canEditNote: "Notizbearbeitung" canInvite: "Erstellung von Einladungscodes für diese Instanz" inviteLimit: "Maximalanzahl an Einladungen" inviteLimitCycle: "Zyklus des Einladungslimits" @@ -1476,6 +1484,7 @@ _role: descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver." canHideAds: "Kann Werbung ausblenden" canSearchNotes: "Nutzung der Notizsuchfunktion" + canUseTranslator: "Verwendung des Ãœbersetzers" _condition: isLocal: "Lokaler Benutzer" isRemote: "Benutzer fremder Instanz" @@ -1524,6 +1533,10 @@ _ad: reduceFrequencyOfThisAd: "Diese Werbung weniger anzeigen" hide: "Ausblenden" timezoneinfo: "Der Wochentag wird durch die Serverzeitzone bestimmt." + adsSettings: "Werbeeinstellungen" + notesPerOneAd: "Werbeintervall während Echtzeitaktualisierung (Notizen pro Werbung)" + setZeroToDisable: "Setze dies auf 0, um Werbung während Echtzeitaktualisierung zu deaktivieren" + adsTooClose: "Durch den momentan sehr niedrigen Werbeintervall kann es zu einer starken Verschlechterung der Benutzererfahrung kommen." _forgotPassword: enterEmail: "Gib die Email-Adresse ein, mit der du dich registriert hast. An diese wird ein Link gesendet, mit dem du dein Passwort zurücksetzen kannst." ifNoEmail: "Solltest du bei der Registrierung keine Email-Adresse angegeben haben, wende dich bitte an den Administrator." @@ -1609,11 +1622,6 @@ _wordMute: muteWords: "Stummgeschaltete Wörter" muteWordsDescription: "Zum Nutzen einer \"UND\"-Verknüpfung Einträge mit Leerzeichen trennen, zum Nutzen einer \"ODER\"-Verknüpfung Einträge mit einem Zeilenumbruch trennen." muteWordsDescription2: "Umgib Schlüsselworter mit Schrägstrichen, um Reguläre Ausdrücke zu verwenden." - softDescription: "Notizen, die die angegebenen Konditionen erfüllen, in der Chronik ausblenden." - hardDescription: "Verhindern, dass Notizen, die die angegebenen Konditionen erfüllen, der Chronik hinzugefügt werden. Zudem werden diese Notizen auch nicht der Chronik hinzugefügt, falls die Konditionen geändert werden." - soft: "Leicht" - hard: "Schwer" - mutedNotes: "Stummgeschaltete Notizen" _instanceMute: instanceMuteDescription: "Schaltet alle Notizen/Renotes stumm, die von den gelisteten Instanzen stammen, inklusive Antworten von Benutzern an einen Benutzer einer stummgeschalteten Instanz." instanceMuteDescription2: "Instanzen getrennt durch Zeilenumbrüchen angeben" @@ -1677,9 +1685,6 @@ _theme: infoFg: "Text von Informationen" infoWarnBg: "Hintergrund von Warnungen" infoWarnFg: "Text von Warnungen" - cwBg: "Hintergrund des Inhaltswarnungsknopfs" - cwFg: "Text des Inhaltswarnungsknopfs" - cwHoverBg: "Hintergrund des Inhaltswarnungsknopfs (Mouseover)" toastBg: "Hintergrund von Benachrichtigungen" toastFg: "Text von Benachrichtigungen" buttonBg: "Hintergrund von Schaltflächen" @@ -1697,8 +1702,6 @@ _sfx: note: "Notizen" noteMy: "Meine Notizen" notification: "Benachrichtigungen" - chat: "Chat" - chatBg: "Chat (Hintergrund)" antenna: "Antennen" channel: "Kanalbenachrichtigung" _ago: @@ -2138,3 +2141,11 @@ _moderationLogTypes: createAd: "Werbung erstellt" deleteAd: "Werbung gelöscht" updateAd: "Werbung aktualisiert" +_fileViewer: + title: "Dateiinformationen" + type: "Dateityp" + size: "Dateigröße" + url: "URL" + uploadedAt: "Hochgeladen am" + attachedNotes: "Zugehörige Notizen" + thisPageCanBeSeenFromTheAuthor: "Nur der Benutzer, der diese Datei hochgeladen hat, kann diese Seite sehen." diff --git a/locales/el-GR.yml b/locales/el-GR.yml index e46efcec1f74bd2035a55f325161645637df4a19..9392fd12feb86a561becb4eff5ac9638a4ad22af 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -303,8 +303,6 @@ _theme: _sfx: note: "Σημειώματα" notification: "Ειδοποιήσεις" - chat: "Συνομιλία" - chatBg: "Συνομιλία (ΠαÏασκήνιο)" antenna: "ΑντÎνες" channel: "Ειδοποιήσεις καναλιών" _ago: diff --git a/locales/en-US.yml b/locales/en-US.yml index 9ed8ad9f238734fdf064f8a8b093e1bde5357d3f..66825eaa7fe108053cb23645a48919586a32a42e 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1126,6 +1126,15 @@ edited: "Edited" notificationRecieveConfig: "Notification Settings" mutualFollow: "Mutual follow" fileAttachedOnly: "Only notes with files" +showRepliesToOthersInTimeline: "Show replies to others in TL" +hideRepliesToOthersInTimeline: "Hide replies to others from TL" +externalServices: "External Services" +impressum: "Impressum" +impressumUrl: "Impressum URL" +impressumDescription: "In some countries, like germany, the inclusion of operator contact information (an Impressum) is legally required for commercial websites." +privacyPolicy: "Privacy Policy" +privacyPolicyUrl: "Privacy Policy URL" +tosAndPrivacyPolicy: "Terms of Service and Privacy Policy" _announcement: forExistingUsers: "Existing users only" forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it." @@ -1456,7 +1465,6 @@ _role: gtlAvailable: "Can view the global timeline" ltlAvailable: "Can view the local timeline" canPublicNote: "Can send public notes" - canEditNote: "Note editing" canInvite: "Can create instance invite codes" inviteLimit: "Invite limit" inviteLimitCycle: "Invite limit cooldown" @@ -1476,6 +1484,7 @@ _role: descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. " canHideAds: "Can hide ads" canSearchNotes: "Usage of note search" + canUseTranslator: "Translator usage" _condition: isLocal: "Local user" isRemote: "Remote user" @@ -1524,6 +1533,10 @@ _ad: reduceFrequencyOfThisAd: "Show this ad less" hide: "Hide" timezoneinfo: "The day of the week is determined from the server's timezone." + adsSettings: "Ad settings" + notesPerOneAd: "Real-time update ad placement interval (Notes per ad)" + setZeroToDisable: "Set this value to 0 to disable real-time update ads" + adsTooClose: "The current ad interval may significantly worsen the user experience due to being too low." _forgotPassword: enterEmail: "Enter the email address you used to register. A link with which you can reset your password will then be sent to it." ifNoEmail: "If you did not use an email during registration, please contact the instance administrator instead." @@ -1609,11 +1622,6 @@ _wordMute: muteWords: "Muted words" muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." muteWordsDescription2: "Surround keywords with slashes to use regular expressions." - softDescription: "Hide notes that fulfil the set conditions from the timeline." - hardDescription: "Prevents notes fulfilling the set conditions from being added to the timeline. In addition, these notes will not be added to the timeline even if the conditions are changed." - soft: "Soft" - hard: "Hard" - mutedNotes: "Muted notes" _instanceMute: instanceMuteDescription: "This will mute any notes/renotes from the listed instances, including those of users replying to a user from a muted instance." instanceMuteDescription2: "Separate with newlines" @@ -1677,9 +1685,6 @@ _theme: infoFg: "Information text" infoWarnBg: "Warning background" infoWarnFg: "Warning text" - cwBg: "CW button background" - cwFg: "CW button text" - cwHoverBg: "CW button background (Hover)" toastBg: "Notification background" toastFg: "Notification text" buttonBg: "Button background" @@ -1697,8 +1702,6 @@ _sfx: note: "New note" noteMy: "Own note" notification: "Notifications" - chat: "Chat" - chatBg: "Chat (Background)" antenna: "Antennas" channel: "Channel notifications" _ago: @@ -2138,3 +2141,11 @@ _moderationLogTypes: createAd: "Ad created" deleteAd: "Ad deleted" updateAd: "Ad updated" +_fileViewer: + title: "File details" + type: "File type" + size: "Filesize" + url: "URL" + uploadedAt: "Uploaded at" + attachedNotes: "Attached notes" + thisPageCanBeSeenFromTheAuthor: "This page can only be seen by the user who uploaded this file." diff --git a/locales/es-ES.yml b/locales/es-ES.yml index d663bd829c5103512fc7a4898b673f5114f84034..3a01f40dfdd8546ee7ae041dd3bd7534ba44e481 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -1603,11 +1603,6 @@ _wordMute: muteWords: "Palabras que silenciar" muteWordsDescription: "Separar con espacios indica una declaracion And, separar con lineas nuevas indica una declaracion Or。" muteWordsDescription2: "Encerrar las palabras clave entre numerales para usar expresiones regulares" - softDescription: "Ocultar en la linea de tiempo las notas que cumplen las condiciones" - hardDescription: "Evitar que se agreguen a la linea de tiempo las notas que cumplen las condiciones. Las notas no agregadas seguirán quitadas aunque cambien las condiciones." - soft: "Suave" - hard: "Duro" - mutedNotes: "Notas silenciadas" _instanceMute: instanceMuteDescription: "Silencia todas las notas y reposts de la instancias seleccionadas, incluyendo respuestas a los usuarios de las mismas" instanceMuteDescription2: "Separar por lÃneas" @@ -1671,9 +1666,6 @@ _theme: infoFg: "Texto de información" infoWarnBg: "Fondo de advertencias" infoWarnFg: "Texto de advertencias" - cwBg: "Fondo del botón CW" - cwFg: "Texto del botón CW" - cwHoverBg: "Fondo del botón CW (hover)" toastBg: "Fondo de notificaciones" toastFg: "Texto de notificaciones" buttonBg: "Fondo de botón" @@ -1691,8 +1683,6 @@ _sfx: note: "Notas" noteMy: "Nota (a mà mismo)" notification: "Notificaciones" - chat: "Chat" - chatBg: "Chat (Fondo)" antenna: "Antena receptora" channel: "Notificaciones del canal" _ago: diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index db19b66880e6bf44b31f8bd9b0720c1d69ef8282..0ad1247ff0727eed2132f499c4ad5a1ed06c3679 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -45,6 +45,7 @@ pin: "Épingler sur le profil" unpin: "Désépingler" copyContent: "Copier le contenu" copyLink: "Copier le lien" +copyLinkRenote: "Copier le lien de la renote" delete: "Supprimer" deleteAndEdit: "Supprimer et réécrire" deleteAndEditConfirm: "Êtes-vous sûr de vouloir effacer cette note et la modifier ? Vous perdrez toutes les réactions, renotes et réponses." @@ -129,6 +130,8 @@ unmarkAsSensitive: "Supprimer le marquage comme sensible" enterFileName: "Entrer le nom du fichier" mute: "Masquer" unmute: "Ne plus masquer" +renoteMute: "Masquer les renotes" +renoteUnmute: "Ne plus masquer les renotes" block: "Bloquer" unblock: "Débloquer" suspend: "Suspendre" @@ -414,6 +417,7 @@ moderator: "Modérateur·rice·s" moderation: "Modérations" moderationNote: "Note de modération" addModerationNote: "Ajouter une note de modération" +moderationLogs: "Journal de modération" nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s" securityKeyAndPasskey: "Sécurité et clés de sécurité" securityKey: "Clé de sécurité" @@ -472,6 +476,7 @@ aboutX: "À propos de {x}" emojiStyle: "Style des émojis" native: "Natif" disableDrawer: "Les menus ne s'affichent pas dans le tiroir" +showNoteActionsOnlyHover: "Afficher les actions de note uniquement au survol" noHistory: "Pas d'historique" signinHistory: "Historique de connexion" enableAdvancedMfm: "Activer la MFM avancée" @@ -647,6 +652,7 @@ behavior: "Comportement" sample: "Exemple" abuseReports: "Signalements" reportAbuse: "Signaler" +reportAbuseRenote: "Signaler la renote" reportAbuseOf: "Signaler {name}" fillAbuseReportDescription: "Veuillez expliquer les raisons du signalement. S'il s'agit d'une note précise, veuillez en donner le lien." abuseReported: "Le rapport est envoyé. Merci." @@ -671,6 +677,8 @@ clip: "Clip" createNew: "Créer nouveau" optional: "Facultatif" createNewClip: "Créer un nouveau clip" +unclip: "Supprimer le clip" +confirmToUnclipAlreadyClippedNote: "Cette note fait déjà partie du clip « {name} ». Souhaitez-vous la supprimer de ce clip ?" public: "Public" private: "Privé" i18nInfo: "Misskey est traduit dans différentes langues par des bénévoles. Vous pouvez contribuer à {link}." @@ -933,12 +941,15 @@ unsubscribePushNotification: "Désactiver les notifications push" pushNotificationAlreadySubscribed: "Les notifications push sont déjà activées" pushNotificationNotSupported: "Votre navigateur ou votre instance ne prend pas en charge les notifications push" sendPushNotificationReadMessage: "Supprimer les notifications push une fois que les notifications ou messages pertinents ont été lus." +windowMaximize: "Maximiser" +windowMinimize: "Minimaliser" windowRestore: "Restaurer" caption: "Libellé" loggedInAsBot: "Connecté actuellement en tant que bot" tools: "Outils" cannotLoad: "Chargement impossible" like: "J'aime" +unlike: "Ne plus aimer" numberOfLikes: "Favoris" show: "Affichage" neverShow: "Ne plus afficher" @@ -949,6 +960,7 @@ noRole: "Aucun rôle" normalUser: "Simple utilisateur·rice" undefined: "Non défini" assign: "Attribuer" +unassign: "Retirer" color: "Couleur" manageCustomEmojis: "Gestion des émojis personnalisés" preset: "Préréglage" @@ -958,12 +970,16 @@ thisPostMayBeAnnoying: "Cette note peut gêner d'autres personnes." thisPostMayBeAnnoyingHome: "Publier vers le fil principal" thisPostMayBeAnnoyingCancel: "Annuler" thisPostMayBeAnnoyingIgnore: "Publier quand-même" +collapseRenotes: "Réduire les renotes déjà vues" internalServerError: "Erreur interne du serveur" copyErrorInfo: "Copier les détails de l’erreur" exploreOtherServers: "Trouver une autre instance" disableFederationOk: "Désactiver" likeOnly: "Les favoris uniquement" +sensitiveWords: "Mots sensibles" +notesSearchNotAvailable: "La recherche de notes n'est pas disponible." license: "Licence" +myClips: "Mes clips" video: "Vidéo" videos: "Vidéos" dataSaver: "Économiseur de données" @@ -973,6 +989,7 @@ accountMovedShort: "Ce compte a migré" operationForbidden: "Opération non autorisée" addMemo: "Ajouter un mémo" reactionsList: "Réactions" +renotesList: "Liste de renotes" notificationDisplay: "Style des notifications" leftTop: "En haut à gauche" rightTop: "En haut à droite" @@ -982,6 +999,7 @@ vertical: "Vertical" horizontal: "Latéral" serverRules: "Règles du serveur" archive: "Archive" +displayOfNote: "Affichage de la note" youFollowing: "Abonné·e" options: "Options" later: "Plus tard" @@ -1001,6 +1019,7 @@ pinnedList: "Liste épinglée" notifyNotes: "Notifier à propos des nouvelles notes" authentication: "Authentification" authenticationRequiredToContinue: "Veuillez vous authentifier pour continuer" +showRenotes: "Afficher les renotes" _announcement: readConfirmTitle: "Marquer comme lu ?" _initialAccountSetting: @@ -1082,12 +1101,20 @@ _achievements: title: "Beaucoup d'amis" _followers10: title: "Abonnez-moi !" + description: "Obtenir plus de 10 abonné·e·s" + _followers50: + description: "Obtenir plus de 50 abonné·e·s" _followers100: title: "Populaire" + description: "Obtenir plus de 100 abonné·e·s" + _followers300: + description: "Obtenir plus de 300 abonné·e·s" _followers500: title: "Tour radio" + description: "Obtenir plus de 500 abonné·e·s" _followers1000: title: "Influenceur·euse" + description: "Obtenir plus de 1000 abonné·e·s" _iLoveMisskey: title: "J’adore Misskey" description: "Publication « J’⤠#Misskey »" @@ -1151,6 +1178,7 @@ _role: high: "Haute" _options: canManageCustomEmojis: "Gestion des émojis personnalisés" + wordMuteMax: "Nombre maximal de caractères dans le filtre de mots" _sensitiveMediaDetection: description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement." sensitivity: "Sensibilité de la détection" @@ -1267,11 +1295,6 @@ _wordMute: muteWords: "Mots à filtrer" muteWordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR." muteWordsDescription2: "Pour utiliser des expressions régulières (regex), mettez les mots-clés entre barres obliques." - softDescription: "Masquez les notes de votre fil selon les paramètres que vous définissez." - hardDescription: "Empêchez votre fil de charger les notes selon les paramètres que vous définissez. Cette action est irréversible : si vous modifiez ces paramètres plus tard, les notes précédemment filtrées ne seront pas récupérées." - soft: "Doux" - hard: "Strict" - mutedNotes: "Notes filtrées" _instanceMute: instanceMuteDescription: "Met en sourdine toutes les notes et renotes de l'instance configurée, y compris les réponses aux utilisateurs de l'instance muette." instanceMuteDescription2: "Séparer avec de nouvelles lignes" @@ -1335,9 +1358,6 @@ _theme: infoFg: "Texte d'information" infoWarnBg: "Arrière-plan des avertissements" infoWarnFg: "Texte d’avertissement" - cwBg: "Arrière-plan du CW" - cwFg: "Texte du bouton CW" - cwHoverBg: "Arrière-plan du bouton CW (survolé)" toastBg: "Arrière-plan de la bulle de notification" toastFg: "Texte de la bulle de notification" buttonBg: "Arrière-plan du bouton" @@ -1355,8 +1375,6 @@ _sfx: note: "Nouvelle note" noteMy: "Ma note" notification: "Notifications" - chat: "Discuter" - chatBg: "Discussion (arrière-plan)" antenna: "Réception de l’antenne" channel: "Notifications de canal" _ago: diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 0e067c5383e5ceca1bfe946af0843dee0458d26d..90bca65119a5d23e7eb3fd186810122ff8309e7f 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -1564,11 +1564,6 @@ _wordMute: muteWords: "Kata yang dibisukan" muteWordsDescription: "Pisahkan dengan spasi untuk kondisi AND. Pisahkan dengan baris baru untuk kondisi OR." muteWordsDescription2: "Kurung kata kunci dengan garis miring untuk menggunakan ekspresi reguler." - softDescription: "Sembunyikan catatan yang memenuhi aturan kondisi dari lini masa." - hardDescription: "Cegah catatan memenuhi aturan kondisi dari ditambahkan ke lini masa. Dengan tambahan, catatan berikut tidak akan ditambahkan ke lini masa meskipun jika kondisi tersebut diubah." - soft: "Lembut" - hard: "Keras" - mutedNotes: "Catatan yang dibisukan" _instanceMute: instanceMuteDescription: "Pengaturan ini akan membisukan note/renote apa saja dari instansi yang terdaftar, termasuk pengguna yang membalas pengguna lain dalam instansi yang dibisukan." instanceMuteDescription2: "Pisah dengan baris baru" @@ -1632,9 +1627,6 @@ _theme: infoFg: "Teks informasi" infoWarnBg: "Latar belakang peringatan" infoWarnFg: "Teks peringatan" - cwBg: "Latar belakang tombol Sembunyikan Konten" - cwFg: "Teks tombol Sembunyikan Konten" - cwHoverBg: "Latar belakang tombol Sembunyikan Konten (Mengambang)" toastBg: "Latar belakang notifikasi" toastFg: "Teks notifikasi" buttonBg: "Latar belakang tombol" @@ -1652,8 +1644,6 @@ _sfx: note: "Catatan" noteMy: "Catatan (Saya)" notification: "Notifikasi" - chat: "Pesan" - chatBg: "Obrolan (Latar Belakang)" antenna: "Penerimaan Antenna" channel: "Notifikasi Kanal" _ago: diff --git a/locales/index.d.ts b/locales/index.d.ts index a9de0ad9652f8b466dbcf7364aa94fe142a138b8..2494c1709b3f0694864531926a8ddb6c0f126d34 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1129,6 +1129,15 @@ export interface Locale { "notificationRecieveConfig": string; "mutualFollow": string; "fileAttachedOnly": string; + "showRepliesToOthersInTimeline": string; + "hideRepliesToOthersInTimeline": string; + "externalServices": string; + "impressum": string; + "impressumUrl": string; + "impressumDescription": string; + "privacyPolicy": string; + "privacyPolicyUrl": string; + "tosAndPrivacyPolicy": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; @@ -1542,7 +1551,6 @@ export interface Locale { "gtlAvailable": string; "ltlAvailable": string; "canPublicNote": string; - "canEditNote": string; "canInvite": string; "inviteLimit": string; "inviteLimitCycle": string; @@ -1619,6 +1627,10 @@ export interface Locale { "reduceFrequencyOfThisAd": string; "hide": string; "timezoneinfo": string; + "adsSettings": string; + "notesPerOneAd": string; + "setZeroToDisable": string; + "adsTooClose": string; }; "_forgotPassword": { "enterEmail": string; @@ -1719,11 +1731,6 @@ export interface Locale { "muteWords": string; "muteWordsDescription": string; "muteWordsDescription2": string; - "softDescription": string; - "hardDescription": string; - "soft": string; - "hard": string; - "mutedNotes": string; }; "_instanceMute": { "instanceMuteDescription": string; @@ -1789,9 +1796,6 @@ export interface Locale { "infoFg": string; "infoWarnBg": string; "infoWarnFg": string; - "cwBg": string; - "cwFg": string; - "cwHoverBg": string; "toastBg": string; "toastFg": string; "buttonBg": string; @@ -1811,8 +1815,6 @@ export interface Locale { "note": string; "noteMy": string; "notification": string; - "chat": string; - "chatBg": string; "antenna": string; "channel": string; }; @@ -2289,6 +2291,15 @@ export interface Locale { "deleteAd": string; "updateAd": string; }; + "_fileViewer": { + "title": string; + "type": string; + "size": string; + "url": string; + "uploadedAt": string; + "attachedNotes": string; + "thisPageCanBeSeenFromTheAuthor": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 83b63e15d21cf03fab9c86c4b54de0be9f01d1dd..fa8670d115a0d14eec116d50b25c6a2bfa36a0dc 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -64,7 +64,7 @@ reply: "Rispondi" loadMore: "Mostra di più" showMore: "Espandi" showLess: "Comprimi" -youGotNewFollower: "Ti sta seguendo" +youGotNewFollower: "Adesso ti segue" receiveFollowRequest: "Hai ricevuto una richiesta di follow" followRequestAccepted: "Ha accettato la tua richiesta di follow" mention: "Menzioni" @@ -78,7 +78,7 @@ download: "Scarica" driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\", e le Note a cui è stato allegato?" unfollowConfirm: "Vuoi davvero smettere di seguire {name}?" exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive." -importRequested: "Hai richiesto un'importazione. Può volerci tempo. " +importRequested: "Hai richiesto un'importazione. Potrebbe richiedere un po' di tempo." lists: "Liste" noLists: "Nessuna lista" note: "Nota" @@ -113,7 +113,7 @@ cantReRenote: "È impossibile rinotare una Rinota." quote: "Cita" inChannelRenote: "Rinota nel canale" inChannelQuote: "Cita nel canale" -pinnedNote: "Nota fissata" +pinnedNote: "Nota in primo piano" pinned: "Fissa sul profilo" you: "Tu" clickToShow: "Clicca per visualizzare" @@ -186,7 +186,7 @@ recipient: "Destinatario" annotation: "Annotazione preventiva" federation: "Federazione" instances: "Istanza" -registeredAt: "Registrato presso" +registeredAt: "Prima federazione" latestRequestReceivedAt: "Ultima richiesta ricevuta" latestStatus: "Ultimo stato" storageUsage: "Capienza dei dischi" @@ -336,7 +336,7 @@ instanceName: "Nome dell'istanza" instanceDescription: "Descrizione dell'istanza" maintainerName: "Nome dell'amministratore" maintainerEmail: "Indirizzo e-mail dell'amministratore" -tosUrl: "URL dei termini del servizio e della privacy" +tosUrl: "URL delle condizioni d'uso" thisYear: "Anno" thisMonth: "Mese" today: "Oggi" @@ -364,7 +364,7 @@ pinnedUsersDescription: "Elenca gli/le utenti che vuoi fissare in cima alla pagi pinnedPages: "Pagine in evidenza" pinnedPagesDescription: "Specifica il percorso delle pagine che vuoi fissare in cima alla pagina dell'istanza. Una pagina per riga." pinnedClipId: "ID della Clip in evidenza" -pinnedNotes: "Nota fissata" +pinnedNotes: "Note in primo piano" hcaptcha: "hCaptcha" enableHcaptcha: "Abilita hCaptcha" hcaptchaSiteKey: "Chiave del sito" @@ -384,7 +384,7 @@ name: "Nome" antennaSource: "Fonte dell'antenna" antennaKeywords: "Parole chiavi da ricevere" antennaExcludeKeywords: "Parole chiavi da escludere" -antennaKeywordsDescription: "Separare con uno spazio indica la condizione \"E\". Separare con un'interruzzione riga indica la condizione \"O\"." +antennaKeywordsDescription: "Sparando con uno spazio indichi la condizione E (and). Separando con un a capo, indichi la condizione O (or)." notifyAntenna: "Invia notifiche delle nuove note" withFileAntenna: "Solo note con file in allegato" enableServiceworker: "Abilita ServiceWorker" @@ -393,7 +393,7 @@ caseSensitive: "Sensibile alla distinzione tra maiuscole e minuscole" withReplies: "Includere le risposte" connectedTo: "Connessione ai seguenti profili:" notesAndReplies: "Note e risposte" -withFiles: "Con file in allegato" +withFiles: "Con allegati" silence: "Silenzia" silenceConfirm: "Vuoi davvero silenziare questo profilo?" unsilence: "Riattiva" @@ -461,7 +461,7 @@ invitationCode: "Codice di invito" checking: "Confermando" available: "Disponibile" unavailable: "Il nome utente è già in uso" -usernameInvalidFormat: "Il nome utente può contenere solo lettere, numeri e '_'" +usernameInvalidFormat: "Il nome utente deve avere solo caratteri alfanumerici e trattino basso '_'" tooShort: "Troppo breve" tooLong: "Troppo lungo" weakPassword: "Password debole" @@ -1121,7 +1121,20 @@ unnotifyNotes: "Interrompi le notifiche di nuove Note" authentication: "Autenticazione" authenticationRequiredToContinue: "Per procedere, è richiesta l'autenticazione" dateAndTime: "Data e Ora" -showRenotes: "Leggi le Rinota" +showRenotes: "Includi le Rinota" +edited: "Modificato" +notificationRecieveConfig: "Preferenze di notifica" +mutualFollow: "Follow reciproco" +fileAttachedOnly: "Solo con allegati" +showRepliesToOthersInTimeline: "Risposte altrui nella TL" +hideRepliesToOthersInTimeline: "Nascondi Riposte altrui nella TL" +externalServices: "Servizi esterni" +impressum: "Dichiarazione di proprietà " +impressumUrl: "URL della dichiarazione di proprietà " +impressumDescription: "La dichiarazione di proprietà , è obbligatoria in alcuni paesi come la Germania (Impressum)." +privacyPolicy: "Informativa sulla privacy" +privacyPolicyUrl: "URL della informativa privacy" +tosAndPrivacyPolicy: "Condizioni d'uso e informativa sulla privacy" _announcement: forExistingUsers: "Solo ai profili attuali" forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." @@ -1451,14 +1464,14 @@ _role: _options: gtlAvailable: "Disponibilità della Timeline Federata" ltlAvailable: "Disponibilità della Timeline Locale" - canPublicNote: "Può scrivere Note con Visibilità Pubblica" - canInvite: "Genera codici di invito all'istanza" + canPublicNote: "Scrivere Note con Visibilità Pubblica" + canInvite: "Generare codici di invito all'istanza" inviteLimit: "Limite di codici invito" inviteLimitCycle: "Intervallo di emissione del codice di invito" inviteExpirationTime: "Scadenza del codice di invito" canManageCustomEmojis: "Gestire le emoji personalizzate" driveCapacity: "Capienza del Drive" - alwaysMarkNsfw: "Imposta sempre come NSFW" + alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)" pinMax: "Quantità massima di Note in primo piano" antennaMax: "Quantità massima di Antenne" wordMuteMax: "Lunghezza massima del filtro parole" @@ -1469,8 +1482,9 @@ _role: userEachUserListsMax: "Quantità massima di profili per lista" rateLimitFactor: "Limite del rapporto" descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più." - canHideAds: "Può nascondere i banner" + canHideAds: "Nascondere i banner" canSearchNotes: "Ricercare nelle Note" + canUseTranslator: "Tradurre le Note" _condition: isLocal: "Profilo locale" isRemote: "Profilo remoto" @@ -1519,6 +1533,10 @@ _ad: reduceFrequencyOfThisAd: "Visualizza questa pubblicità meno spesso" hide: "Nascondi" timezoneinfo: "Il giorno della settimana è determinato in base al fuso orario del server." + adsSettings: "Impostazioni banner" + notesPerOneAd: "Quantità di Note tra i banner" + setZeroToDisable: "Imposta 0 (zero) per disattivare la distribuzione dei banner durante gli aggiornamenti in tempo reale" + adsTooClose: "Attenzione, l'intervallo di pubblicazione dei banner è molto breve, potrebbe infastidire significativamente la fruizione" _forgotPassword: enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo." ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza." @@ -1530,7 +1548,7 @@ _gallery: unlike: "Non mi piace più" _email: _follow: - title: "Ha iniziato a seguirti" + title: "Adesso ti segue" _receiveFollowRequest: title: "Hai ricevuto una richiesta di follow" _plugin: @@ -1602,13 +1620,8 @@ _menuDisplay: hide: "Nascondere" _wordMute: muteWords: "Parole da filtrare" - muteWordsDescription: "Separare con uno spazio indica la condizione \"E\". Separare con una interruzione di riga, indica la condizione \"O\"" + muteWordsDescription: "Sparando con uno spazio indichi la condizione E (and). Separando con un a capo, indichi la condizione O (or)." muteWordsDescription2: "Se vuoi indicare delle Espressioni Regolari (regexp), metti la condizione all'interno di due slash (/)" - softDescription: "Verranno nascoste da tutte le Timeline quelle Note che soddisfano le seguenti condizioni" - hardDescription: "Impedisci alla istanza di caricare Note che soddisfano le seguenti condizioni. Le Note già filtrate sono già scomparse in modo irreversibile, fino al cambiamento delle condizioni. Dopo di che scompariranno quelle che soddisfano le nuove condizioni." - soft: "Leggero" - hard: "Pesante" - mutedNotes: "Note filtrate" _instanceMute: instanceMuteDescription: "Disattiva tutte le note, le note di rinvio (condivisione) dell'istanza configurata, comprese le risposte agli utenti dell'istanza." instanceMuteDescription2: "Impostazione separata da una nuova riga" @@ -1617,7 +1630,7 @@ _instanceMute: _theme: explore: "Esplora temi" install: "Installa un tema" - manage: "Gerisci temi" + manage: "Gestione temi" code: "Codice tema" description: "Descrizione" installed: "{name} è installato" @@ -1672,9 +1685,6 @@ _theme: infoFg: "Testo di informazioni" infoWarnBg: "Sfondo degli avvisi" infoWarnFg: "Testo di avviso" - cwBg: "Sfondo del CW" - cwFg: "Testo del pulsante CW" - cwHoverBg: "Sfondo del pulsante CW (sorvolato)" toastBg: "Sfondo di notifica a comparsa" toastFg: "Testo di notifica a comparsa" buttonBg: "Sfondo del pulsante" @@ -1692,8 +1702,6 @@ _sfx: note: "Nota" noteMy: "Mia nota" notification: "Notifiche" - chat: "Messaggi" - chatBg: "Chat (sfondo)" antenna: "Ricezione dell'antenna" channel: "Notifiche di canale" _ago: @@ -1878,7 +1886,7 @@ _visibility: followersDescription: "Visibile solo ai tuoi follower" specified: "Nota diretta" specifiedDescription: "Visibile solo ai profili menzionati" - disableFederation: "Non federare" + disableFederation: "Senza federazione" disableFederationDescription: "Non spedire attività alle altre istanze remote" _postForm: replyPlaceholder: "Rispondi a questa nota..." @@ -2018,7 +2026,7 @@ _notification: youGotReply: "{name} ti ha risposto" youGotQuote: "{name} ha citato la tua Nota e ha detto" youRenoted: "{name} ha rinotato" - youWereFollowed: "Ha iniziato a seguirti" + youWereFollowed: "Adesso ti segue" youReceivedFollowRequest: "Hai ricevuto una richiesta di follow" yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata" pollEnded: "Risultati del sondaggio." @@ -2130,3 +2138,6 @@ _moderationLogTypes: unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito" resolveAbuseReport: "Segnalazione risolta" createInvitation: "Genera codice di invito" + createAd: "Banner creato" + deleteAd: "Banner eliminato" + updateAd: "Banner aggiornato" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c4594987296ce2af7c60e3096672044adab8419a..9adc4381a71618e82c31b4f8c67ae47633b41bc3 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1126,6 +1126,15 @@ edited: "編集済ã¿" notificationRecieveConfig: "通知ã®å—ä¿¡è¨å®š" mutualFollow: "相互フォãƒãƒ¼" fileAttachedOnly: "ファイル付ãã®ã¿" +showRepliesToOthersInTimeline: "TLã«ä»–ã®äººã¸ã®è¿”ä¿¡ã‚’å«ã‚ã‚‹" +hideRepliesToOthersInTimeline: "TLã«ä»–ã®äººã¸ã®è¿”ä¿¡ã‚’å«ã‚ãªã„" +externalServices: "外部サービス" +impressum: "é‹å–¶è€…æƒ…å ±" +impressumUrl: "é‹å–¶è€…æƒ…å ±URL" +impressumDescription: "ドイツãªã©ã®ä¸€éƒ¨ã®å›½ã¨åœ°åŸŸã§ã¯è¡¨ç¤ºãŒç¾©å‹™ä»˜ã‘られã¦ã„ã¾ã™(Impressum)。" +privacyPolicy: "プライãƒã‚·ãƒ¼ãƒãƒªã‚·ãƒ¼" +privacyPolicyUrl: "プライãƒã‚·ãƒ¼ãƒãƒªã‚·ãƒ¼URL" +tosAndPrivacyPolicy: "利用è¦ç´„・プライãƒã‚·ãƒ¼ãƒãƒªã‚·ãƒ¼" _announcement: forExistingUsers: "æ—¢å˜ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã¿" @@ -1463,7 +1472,6 @@ _role: gtlAvailable: "ã‚°ãƒãƒ¼ãƒãƒ«ã‚¿ã‚¤ãƒ ラインã®é–²è¦§" ltlAvailable: "ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインã®é–²è¦§" canPublicNote: "パブリック投稿ã®è¨±å¯" - canEditNote: "ノートã®ç·¨é›†" canInvite: "サーãƒãƒ¼æ‹›å¾…コードã®ç™ºè¡Œ" inviteLimit: "招待コードã®ä½œæˆå¯èƒ½æ•°" inviteLimitCycle: "招待コードã®ç™ºè¡Œé–“éš”" @@ -1538,6 +1546,10 @@ _ad: reduceFrequencyOfThisAd: "ã“ã®åºƒå‘Šã®è¡¨ç¤ºé »åº¦ã‚’下ã’ã‚‹" hide: "表示ã—ãªã„" timezoneinfo: "曜日ã¯ã‚µãƒ¼ãƒãƒ¼ã®ã‚¿ã‚¤ãƒ ゾーンを元ã«æŒ‡å®šã•ã‚Œã¾ã™ã€‚" + adsSettings: "広告é…ä¿¡è¨å®š" + notesPerOneAd: "リアルタイム更新ä¸ã«åºƒå‘Šã‚’é…ä¿¡ã™ã‚‹é–“隔(ノートã®å€‹æ•°ï¼‰" + setZeroToDisable: "0ã§ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ 更新時ã®åºƒå‘Šé…信を無効" + adsTooClose: "広告ã®é…ä¿¡é–“éš”ãŒæ¥µã‚ã¦çŸã„ãŸã‚ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ä½“験ãŒè‘—ã—ãæã‚れるå¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚" _forgotPassword: enterEmail: "アカウントã«ç™»éŒ²ã—ãŸãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã‚’入力ã—ã¦ãã ã•ã„。ãã®ã‚¢ãƒ‰ãƒ¬ã‚¹å®›ã¦ã«ã€ãƒ‘スワードリセット用ã®ãƒªãƒ³ã‚¯ãŒé€ä¿¡ã•ã‚Œã¾ã™ã€‚" @@ -1636,11 +1648,6 @@ _wordMute: muteWords: "ミュートã™ã‚‹ãƒ¯ãƒ¼ãƒ‰" muteWordsDescription: "スペースã§åŒºåˆ‡ã‚‹ã¨AND指定ã«ãªã‚Šã€æ”¹è¡Œã§åŒºåˆ‡ã‚‹ã¨OR指定ã«ãªã‚Šã¾ã™ã€‚" muteWordsDescription2: "ã‚ーワードをスラッシュã§å›²ã‚€ã¨æ£è¦è¡¨ç¾ã«ãªã‚Šã¾ã™ã€‚" - softDescription: "指定ã—ãŸæ¡ä»¶ã®ãƒŽãƒ¼ãƒˆã‚’タイムラインã‹ã‚‰éš ã—ã¾ã™ã€‚" - hardDescription: "指定ã—ãŸæ¡ä»¶ã®ãƒŽãƒ¼ãƒˆã‚’タイムラインã«è¿½åŠ ã—ãªã„よã†ã«ã—ã¾ã™ã€‚è¿½åŠ ã•ã‚Œãªã‹ã£ãŸãƒŽãƒ¼ãƒˆã¯ã€æ¡ä»¶ã‚’変更ã—ã¦ã‚‚除外ã•ã‚ŒãŸã¾ã¾ã«ãªã‚Šã¾ã™ã€‚" - soft: "ソフト" - hard: "ãƒãƒ¼ãƒ‰" - mutedNotes: "ミュートã•ã‚ŒãŸãƒŽãƒ¼ãƒˆ" _instanceMute: instanceMuteDescription: "ミュートã—ãŸã‚µãƒ¼ãƒãƒ¼ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¸ã®è¿”ä¿¡ã‚’å«ã‚ã¦ã€è¨å®šã—ãŸã‚µãƒ¼ãƒãƒ¼ã®å…¨ã¦ã®ãƒŽãƒ¼ãƒˆã¨Renoteをミュートã—ã¾ã™ã€‚" @@ -1707,9 +1714,6 @@ _theme: infoFg: "æƒ…å ±ã®æ–‡å—" infoWarnBg: "è¦å‘Šã®èƒŒæ™¯" infoWarnFg: "è¦å‘Šã®æ–‡å—" - cwBg: "CW ボタンã®èƒŒæ™¯" - cwFg: "CW ボタンã®æ–‡å—" - cwHoverBg: "CW ボタンã®èƒŒæ™¯ (ホãƒãƒ¼)" toastBg: "通知トーストã®èƒŒæ™¯" toastFg: "通知トーストã®æ–‡å—" buttonBg: "ボタンã®èƒŒæ™¯" @@ -1728,8 +1732,6 @@ _sfx: note: "ノート" noteMy: "ノート(自分)" notification: "通知" - chat: "ãƒãƒ£ãƒƒãƒˆ" - chatBg: "ãƒãƒ£ãƒƒãƒˆ(ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰)" antenna: "アンテナå—ä¿¡" channel: "ãƒãƒ£ãƒ³ãƒãƒ«é€šçŸ¥" @@ -2201,3 +2203,12 @@ _moderationLogTypes: createAd: "広告を作æˆ" deleteAd: "広告を削除" updateAd: "広告を更新" + +_fileViewer: + title: "ファイルã®è©³ç´°" + type: "ファイルタイプ" + size: "ファイルサイズ" + url: "URL" + uploadedAt: "è¿½åŠ æ—¥" + attachedNotes: "添付ã•ã‚Œã¦ã„るノート" + thisPageCanBeSeenFromTheAuthor: "ã“ã®ãƒšãƒ¼ã‚¸ã¯ã€ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’アップãƒãƒ¼ãƒ‰ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã—ã‹é–²è¦§ã§ãã¾ã›ã‚“。" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index bf945088f0ff74aef9932de28b7babe263948c32..5efd8cd1191ad5e836e430bcc09d71ac75c54b9d 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1586,11 +1586,6 @@ _wordMute: muteWords: "ミュートã™ã‚‹ãƒ¯ãƒ¼ãƒ‰" muteWordsDescription: "スペースã§åŒºåˆ‡ã‚‹ã¨AND指定ã«ãªã£ã¦ã€æ”¹è¡Œã§åŒºåˆ‡ã‚‹ã¨OR指定ã«ãªã‚‹ã§ã€‚" muteWordsDescription2: "ã‚ーワードをスラッシュã§å›²ã‚€ã¨æ£è¦è¡¨ç¾ã«ãªã‚‹ã§ã€‚" - softDescription: "指定ã—ãŸæ¡ä»¶ã®ãƒŽãƒ¼ãƒˆã‚’タイムラインã‹ã‚‰éš ã™ã§ã€‚" - hardDescription: "指定ã—ãŸæ¡ä»¶ã®ãƒŽãƒ¼ãƒˆã‚’タイムラインã«è¿½åŠ ã—ãªã„よã†ã«ã™ã‚‹ã§ã€‚è¿½åŠ ã›ãƒ¼ã¸ã‚“ã‹ã£ãŸã‹ã£ãŸãƒŽãƒ¼ãƒˆã¯ã€æ¡ä»¶ã‚’変ãˆã¦ã‚‚除外ã•ã‚ŒãŸã¾ã¾ã«ãªã‚‹ã§ã€‚" - soft: "ソフト" - hard: "ãƒãƒ¼ãƒ‰" - mutedNotes: "ミュートã•ã‚ŒãŸãƒŽãƒ¼ãƒˆ" _instanceMute: instanceMuteDescription: "ミュートã—ãŸã‚µãƒ¼ãƒãƒ¼ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¸ã®è¿”ä¿¡ã‚’å«ã‚ã¦ã€è¨å®šã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®å…¨ã¦ã®ãƒŽãƒ¼ãƒˆã¨Renoteをミュートã«ã™ã‚‹ã§ã€‚" instanceMuteDescription2: "改行ã§åŒºåˆ‡ã£ã¦è¨å®šã™ã‚‹ã‚“ã‚„ã§" @@ -1654,9 +1649,6 @@ _theme: infoFg: "æƒ…å ±ã®æ–‡å—" infoWarnBg: "è¦å‘Šã®èƒŒæ™¯" infoWarnFg: "è¦å‘Šã®æ–‡å—" - cwBg: "CW ボタンã®èƒŒæ™¯" - cwFg: "CW ボタンã®æ–‡å—" - cwHoverBg: "CW ボタンã®èƒŒæ™¯ (ホãƒãƒ¼)" toastBg: "通知トーストã®èƒŒæ™¯" toastFg: "通知トーストã®æ–‡å—" buttonBg: "ボタンã®èƒŒæ™¯" @@ -1674,8 +1666,6 @@ _sfx: note: "ノート" noteMy: "ノート(自分)" notification: "通知" - chat: "ãƒãƒ£ãƒƒãƒˆ" - chatBg: "ãƒãƒ£ãƒƒãƒˆ(ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰)" antenna: "アンテナå—ä¿¡" channel: "ãƒãƒ£ãƒ³ãƒãƒ«é€šçŸ¥" _ago: diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index af7afb2c3ecd766faccb3ff686ea545a168f8c46..d5c346717b6b3ce8b42841089707dfc0195ef5ff 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1600,11 +1600,6 @@ _wordMute: muteWords: "ë®¤íŠ¸í• ë‹¨ì–´" muteWordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 ORë¡œ ì§€ì •ë©ë‹ˆë‹¤." muteWordsDescription2: "ì •ê·œ 표현ì‹ì„ ì‚¬ìš©í•˜ë ¤ë©´ 키워드를 빗금표(/)ë¡œ ê°ì‹¸ 주세요." - softDescription: "ì§€ì •í•œ ì¡°ê±´ì˜ ë…¸íŠ¸ë¥¼ 타임ë¼ì¸ì—ì„œ 숨ê¹ë‹ˆë‹¤." - hardDescription: "ì§€ì •í•œ ì¡°ê±´ì˜ ë…¸íŠ¸ë¥¼ 타임ë¼ì¸ì— 추가하지 않습니다. 타임ë¼ì¸ì— 추가ë˜ì§€ ì•Šì€ ë…¸íŠ¸ëŠ” ì¡°ê±´ì„ ë³€ê²½í•´ë„ í‘œì‹œë˜ì§€ 않습니다." - soft: "보통" - hard: "보다 ë†’ì€ ìˆ˜ì¤€" - mutedNotes: "ë®¤íŠ¸ëœ ë…¸íŠ¸" _instanceMute: instanceMuteDescription: "뮤트한 서버ì—ì„œ 오는 ë‹µê¸€ì„ í¬í•¨í•œ ëª¨ë“ ë…¸íŠ¸ì™€ Renote를 뮤트합니다." instanceMuteDescription2: "í•œ ì¤„ì— í•˜ë‚˜ì”© ìž…ë ¥í•´ 주세요" @@ -1668,9 +1663,6 @@ _theme: infoFg: "ì •ë³´ì°½ í…스트" infoWarnBg: "ê²½ê³ ì°½ ë°°ê²½" infoWarnFg: "ê²½ê³ ì°½ í…스트" - cwBg: "CW 버튼 ë°°ê²½" - cwFg: "CW 버튼 í…스트" - cwHoverBg: "CW 버튼 ë°°ê²½ (호버)" toastBg: "알림창 ë°°ê²½" toastFg: "알림창 í…스트" buttonBg: "버튼 ë°°ê²½" @@ -1688,8 +1680,6 @@ _sfx: note: "새 노트" noteMy: "ë‚´ 노트" notification: "알림" - chat: "대화" - chatBg: "대화 (백그ë¼ìš´ë“œ)" antenna: "안테나 ìˆ˜ì‹ " channel: "ì±„ë„ ì•Œë¦¼" _ago: diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 22cb5857f901f98fac6d9d74aab58dcf199c409f..b22e047cfa7d3faa9865fc3d6f261e24549f5a06 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -407,7 +407,6 @@ _theme: _sfx: note: "ບັນທຶàº" notification: "àºàº²àº™à»àºˆà»‰àº‡à»€àº•àº·àºàº™" - chat: "à»àºŠà»‹àº”" _2fa: renewTOTPCancel: "ບà»à»ˆâ€‹à»àº¡à»ˆàº™â€‹àº•àºàº™â€‹àº™àºµà»‰" _widgets: diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index fd9ffa33f294d6d9d28d140902d6724d4b914a4e..6f789dff10858714d851ca6ab43d086bda9c3b9b 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -438,7 +438,6 @@ _theme: _sfx: note: "Notities" notification: "Meldingen" - chat: "Chat" _2fa: renewTOTPCancel: "Nee, bedankt" _widgets: diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 00f22c0c4ffeeada5381958fe6ad2f036d0fac8e..d99c61c1dd75ff9aaf8884eb03b3af63ff390254 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -575,9 +575,6 @@ _channel: nameAndDescription: "Navn og beskrivelse" _menuDisplay: hide: "Skjul" -_wordMute: - soft: "Myk" - hard: "Hard" _theme: description: "Beskrivelse" color: "Farge" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 1c7ebe81085b9767811d991e2cba98ce1fb344d6..f88055cc293c91392029a20d544d64c84514f40e 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -982,9 +982,6 @@ _menuDisplay: _wordMute: muteWords: "SÅ‚owo do wyciszenia" muteWordsDescription2: "Otocz sÅ‚owa kluczowe ukoÅ›nikami, aby używać wyrażeÅ„ regularnych." - soft: "Åagodny" - hard: "Twardy" - mutedNotes: "Wyciszone wpisy" _instanceMute: title: "Ukrywa wpisy z wymienionych instancji." heading: "Lista instancji do wyciszenia" @@ -1046,9 +1043,6 @@ _theme: infoFg: "Tekst informacji" infoWarnBg: "TÅ‚o ostrzeżenia" infoWarnFg: "Tekst ostrzeżenia" - cwBg: "TÅ‚o CW" - cwFg: "Tekst CW" - cwHoverBg: "TÅ‚o CW (po najechaniu)" toastBg: "TÅ‚o powiadomieÅ„" toastFg: "Tekst powiadomieÅ„" buttonBg: "TÅ‚o przycisku" @@ -1066,8 +1060,6 @@ _sfx: note: "Wpisy" noteMy: "Mój wpis" notification: "Powiadomienia" - chat: "WiadomoÅ›ci" - chatBg: "Rozmowy (tÅ‚o)" antenna: "Anteny" channel: "Powiadomienia kanaÅ‚u" _ago: diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index f9e777bc75739efa47fd885eaf31cb19e09009a9..23864df1b8f50791b8c18a0d86c93687f04a55d1 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -1320,7 +1320,6 @@ _theme: _sfx: note: "Posts" notification: "Notificações" - chat: "Chat" _ago: invalid: "Não há nada aqui" _timelineTutorial: diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 51c33085afed9b3008f4ca5d17ffe63482b4e283..77bccb7e6b1312b819df9ceb3c4e5daf35bec763 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -647,7 +647,6 @@ _theme: _sfx: note: "Note" notification: "Notificări" - chat: "Chat" _ago: invalid: "Nu e nimic de văzut aici" _widgets: diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 937158978d1712ab31d161083281fc6557990ccc..19e4baccb1a93d749f731c9e101af61d8991561e 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1488,11 +1488,6 @@ _wordMute: muteWords: "Скрыть Ñлово" muteWordsDescription: "Пишите Ñлова через пробел в одной Ñтроке, чтобы фильтровать их поÑвление вмеÑте; а еÑли хотите фильтровать любое из них, пишите в отдельных Ñтроках." muteWordsDescription2: "ЗдеÑÑŒ можно иÑпользовать регулÑрные Ð²Ñ‹Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ â€” проÑто заключите их между Ð´Ð²ÑƒÐ¼Ñ Ð´Ñ€Ð¾Ð±Ð½Ñ‹Ð¼Ð¸ чертами (/)." - softDescription: "СоответÑтвующие уÑловиÑм заметки будут ÑпрÑтаны из вашей ленты." - hardDescription: "СоответÑтующие уÑловиÑм заметки вообще не будут попадать в вашу ленту. Даже еÑли вы поменÑете уÑловиÑ, отÑеенные таким образом заметки уже не поÑвÑÑ‚ÑÑ." - soft: "ÐœÑгко" - hard: "ЖёÑтко" - mutedNotes: "Скрытые заметки" _instanceMute: instanceMuteDescription: "Заметки и репоÑÑ‚Ñ‹ Ñ ÑƒÐºÐ°Ð·Ð°Ð½Ð½Ñ‹Ñ… здеÑÑŒ инÑтанÑов, а также ответы пользователÑм оттуда же не будут отображатьÑÑ." instanceMuteDescription2: "Пишите каждый инÑÑ‚Ð°Ð½Ñ Ð½Ð° отдельной Ñтроке" @@ -1556,9 +1551,6 @@ _theme: infoFg: "ТекÑÑ‚ ÑообщениÑ" infoWarnBg: "Фон предупреждениÑ" infoWarnFg: "ТекÑÑ‚ предупреждениÑ" - cwBg: "Фон Ð¿Ñ€ÐµÐ´ÑƒÐ¿Ñ€ÐµÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¾ Ñодержимом" - cwFg: "ТекÑÑ‚ Ð¿Ñ€ÐµÐ´ÑƒÐ¿Ñ€ÐµÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¾ Ñодержимом" - cwHoverBg: "Фон Ð¿Ñ€ÐµÐ´ÑƒÐ¿Ñ€ÐµÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¾ Ñодержимом (под указателем)" toastBg: "Фон оповещениÑ" toastFg: "ТекÑÑ‚ оповещениÑ" buttonBg: "Фон кнопки" @@ -1576,8 +1568,6 @@ _sfx: note: "Заметки" noteMy: "СобÑтвенные заметки" notification: "УведомлениÑ" - chat: "СообщениÑ" - chatBg: "Ð¡Ð¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ (фон)" antenna: "Ðнтенна" channel: "Канал" _ago: diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index e44aaafc0ad67bac942e23c51acd3100a9e13ce1..181e725d758ac1f2edbb9fff4c96a00ead023115 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -1039,11 +1039,6 @@ _wordMute: muteWords: "UmlÄané slová" muteWordsDescription: "Medzerami oddeľte pre podmienku AND a novými riadkami pre podmienku OR." muteWordsDescription2: "Regulárne výrazy sa použijú keÄ použijete okolo lomÃtka." - softDescription: "Skryje poznámky z Äasovej osi, ktoré spĺňajú podmienky." - hardDescription: "Zabráni poznámky spĺňajúce množinu podmienok, aby boli pridané do Äasovej osi. NavyÅ¡e tieto poznámky nepribudnú v Äasovej osi ani keÄ sa podmienky zmenia." - soft: "Mäkké" - hard: "Tvrdé" - mutedNotes: "UmlÄané poznámky" _instanceMute: instanceMuteDescription: "Toto umlÄà vÅ¡etky poznámky/preposlania zo zoznamu serverov, vrátane tých, na ktoré použÃvatelia odpovedajú z umlÄaného servera." instanceMuteDescription2: "Oddeľte novými riadkami" @@ -1107,9 +1102,6 @@ _theme: infoFg: "InformaÄný text" infoWarnBg: "Pozadie varovania" infoWarnFg: "Text varovania" - cwBg: "CW pozadie tlaÄidla" - cwFg: "CW text tlaÄidla" - cwHoverBg: "CW pozadie tlaÄidla (pod kurzorom)" toastBg: "Pozadie upozornenia" toastFg: "Text upozornenia" buttonBg: "Pozadie tlaÄidla" @@ -1127,8 +1119,6 @@ _sfx: note: "Poznámky" noteMy: "Vlastná poznámka" notification: "Oznámenia" - chat: "Chat" - chatBg: "Chat (pozadie)" antenna: "Antény" channel: "Upozornenia kanála" _ago: diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index 62e7d412abd563e5d190723d15b7c29a9c52dc52..92678afef8063643af6fbdd07c8aafbe147c4541 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -507,7 +507,6 @@ _theme: _sfx: note: "Noter" notification: "Notifikationer" - chat: "Chatt" antenna: "Antenner" _2fa: renewTOTPCancel: "Nej tack" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index c2adcf8ec59688120344424c230a86c299c143a6..1c655f5886d1c3a610f07d1d3bf22b10548749f9 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1150,6 +1150,7 @@ _serverRules: description: "ชุดขà¸à¸‡à¸à¸Žà¸—ี่จะà¹à¸ªà¸”งà¸à¹ˆà¸à¸™à¸à¸²à¸£à¸¥à¸‡à¸—ะเบียนเราขà¸à¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¸•à¸±à¹‰à¸‡à¸„่าสรุปข้à¸à¸à¸³à¸«à¸™à¸”ในà¸à¸²à¸£à¹ƒà¸«à¹‰à¸šà¸£à¸´à¸à¸²à¸£" _serverSettings: iconUrl: "ไà¸à¸„à¸à¸™ URL" + manifestJsonOverride: "manifest.json โà¸à¹€à¸§à¸à¸£à¹Œà¸¥à¸²à¸¢" shortName: "ชื่à¸à¸¢à¹ˆà¸" _accountMigration: moveFrom: "ย้ายข้à¸à¸¡à¸¹à¸¥à¸šà¸±à¸à¸Šà¸µà¸à¸·à¹ˆà¸™à¹„ปยังà¸à¸µà¸à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰à¸«à¸™à¸¶à¹ˆà¸‡" @@ -1407,6 +1408,7 @@ _achievements: flavor: "Misskey-Misskey La-Tu-Ma" _smashTestNotificationButton: title: "ทดสà¸à¸šà¹‚à¸à¹€à¸§à¸à¸£à¹Œà¹‚ฟลว์" + description: "ทดสà¸à¸šà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸—ริà¸à¹€à¸à¸à¸£à¹Œà¸‹à¹‰à¸³à¹† ภายในระยะเวลาà¸à¸±à¸™à¸ªà¸±à¹‰à¸™à¹†" _role: new: "บทบาทใหม่" edit: "à¹à¸à¹‰à¹„ขบทบาท" @@ -1445,7 +1447,6 @@ _role: gtlAvailable: "à¸à¸²à¸£à¸”ูไทม์ไลน์ทั่วโลà¸" ltlAvailable: "à¸à¸²à¸£à¸”ูไทม์ไลน์ในท้à¸à¸‡à¸–ิ่น" canPublicNote: "สามารถส่งโน้ตสาธารณะ" - canEditNote: "à¸à¸³à¸¥à¸±à¸‡à¹à¸à¹‰à¹„ขโน้ต" canInvite: "สร้างรหัสเชิà¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" inviteLimit: "จำà¸à¸±à¸”à¸à¸²à¸£à¹€à¸Šà¸´à¸" inviteLimitCycle: "จำà¸à¸±à¸”à¸à¸²à¸£à¹€à¸Šà¸´à¸à¹„ว้คูลดาวน์" @@ -1465,6 +1466,7 @@ _role: descriptionOfRateLimitFactor: "ขีดจà¹à¸²à¸à¸±à¸”à¸à¸±à¸•à¸£à¸²à¸—ี่ต่ำà¸à¸§à¹ˆà¸²à¸¡à¸µà¸‚้à¸à¸ˆà¹à¸²à¸à¸±à¸”น้à¸à¸¢à¸à¸§à¹ˆà¸²à¸‚้à¸à¸ˆà¹à¸²à¸à¸±à¸”ที่สูงà¸à¸§à¹ˆà¸²" canHideAds: "ซ่à¸à¸™à¹‚ฆษณา" canSearchNotes: "à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸à¸²à¸£à¸„้นหาโน้ต" + canUseTranslator: "à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¹à¸›à¸¥" _condition: isLocal: "ผู้ใช้ภายใน" isRemote: "ผู้ใช้ระยะไà¸à¸¥" @@ -1598,11 +1600,6 @@ _wordMute: muteWords: "ปิดเสียงคำ" muteWordsDescription: "คั่นด้วยช่à¸à¸‡à¸§à¹ˆà¸²à¸‡à¸ªà¸³à¸«à¸£à¸±à¸šà¹€à¸‡à¸·à¹ˆà¸à¸™à¹„ข AND หรืà¸à¸”้วยà¸à¸²à¸£à¸‚ึ้นบรรทัดใหม่สำหรับเงื่à¸à¸™à¹„ข OR นะ" muteWordsDescription2: "ล้à¸à¸¡à¸£à¸à¸šà¸„ีย์เวิร์ดด้วยเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸—ับเพื่à¸à¹ƒà¸Šà¹‰à¸™à¸´à¸žà¸ˆà¸™à¹Œà¸—ั่วไป" - softDescription: "ซ่à¸à¸™à¹‚น้ตให้ตรงตามเงื่à¸à¸™à¹„ขที่ตั้งไว้จาà¸à¹„ทม์ไลน์" - hardDescription: "ป้à¸à¸‡à¸à¸±à¸™à¹„ม่ให้โน้ตย่à¸à¸—ี่ตรงตามเงื่à¸à¸™à¹„ขที่ตั้งไว้ไม่ให้ถูà¸à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸‡à¹ƒà¸™à¹„ทม์ไลน์ นà¸à¸à¸ˆà¸²à¸à¸™à¸µà¹‰ โน้ตเหล่านี้จะไม่ถูà¸à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸‡à¹ƒà¸™à¹„ทม์ไลน์à¹à¸¡à¹‰à¸§à¹ˆà¸²à¸ˆà¸°à¸¡à¸µà¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¹€à¸‡à¸·à¹ˆà¸à¸™à¹„ขยังไงà¸à¹‡à¸•à¸²à¸¡" - soft: "ซà¸à¸Ÿ" - hard: "ยาà¸" - mutedNotes: "ปิดเสียงโน้ต" _instanceMute: instanceMuteDescription: "à¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¸™à¸µà¹‰à¸ˆà¸°à¸›à¸´à¸”เสียง\"โน้ต/รีโน้ต\"จาà¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่à¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¸£à¸²à¸¢à¸à¸²à¸£ รวมถึงบันทึà¸à¸‚à¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ตà¸à¸šà¸à¸¥à¸±à¸šà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸ˆà¸²à¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่ปิดเสียง" instanceMuteDescription2: "คั่นด้วยà¸à¸²à¸£à¸‚ึ้นบรรทัดใหม่" @@ -1666,9 +1663,6 @@ _theme: infoFg: "ข้à¸à¸„วามข้à¸à¸¡à¸¹à¸¥" infoWarnBg: "คำเตืà¸à¸™à¸žà¸·à¹‰à¸™à¸«à¸¥à¸±à¸‡" infoWarnFg: "คำเตืà¸à¸™à¸‚้à¸à¸„วาม" - cwBg: "ปุ่ม CW พื้นหลัง" - cwFg: "ปุ่ม CW ข้à¸à¸„วาม" - cwHoverBg: "ปุ่ม CW พื้นหลัง (โฮเวà¸à¸£à¹Œ)" toastBg: "ประวัติà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" toastFg: "ข้à¸à¸„วามà¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" buttonBg: "ปุ่มพื้นหลัง" @@ -1686,8 +1680,6 @@ _sfx: note: "หมายเหตุ" noteMy: "โน้ตขà¸à¸‡à¸•à¸±à¸§à¹€à¸à¸‡" notification: "à¸à¸²à¸£à¹€à¹€à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" - chat: "à¹à¸Šà¸—" - chatBg: "à¹à¸Šà¸— (พื้นหลัง)" antenna: "เสาà¸à¸²à¸à¸²à¸¨" channel: "à¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸Šà¹ˆà¸à¸‡" _ago: @@ -1792,6 +1784,7 @@ _antennaSources: homeTimeline: "โน้ตจาà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ติดตาม" users: "โน้ตจาà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่เฉพาะเจาะจง" userList: "โน้ตจาà¸à¸£à¸²à¸¢à¸Šà¸·à¹ˆà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ระบุ" + userBlacklist: "โน้ตทั้งหมดยà¸à¹€à¸§à¹‰à¸™à¹‚น้ตขà¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ต้à¸à¸‡à¸£à¸°à¸šà¸¸à¹€à¸ˆà¸²à¸°à¸ˆà¸‡à¸•à¸±à¹‰à¸‡à¹à¸•à¹ˆà¸«à¸™à¸¶à¹ˆà¸‡à¸£à¸²à¸¢à¸‚ึ้นไป" _weekday: sunday: "วันà¸à¸²à¸—ิตย์" monday: "วันจันทร์" @@ -1891,6 +1884,7 @@ _profile: metadataContent: "เนื้à¸à¸«à¸²" changeAvatar: "เปลี่ยนà¸à¸§à¸²à¸•à¸²à¸£à¹Œ" changeBanner: "เปลี่ยนà¹à¸šà¸™à¹€à¸™à¸à¸£à¹Œ" + verifiedLinkDescription: "โดยà¸à¸²à¸£à¸›à¹‰à¸à¸™ URL ที่มีลิงà¸à¹Œà¹„ปยังโปรไฟล์ขà¸à¸‡à¸„ุณตรงนี้ ส่วนไà¸à¸„à¸à¸™à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸„วามเป็นเจ้าขà¸à¸‡à¸™à¸±à¹‰à¸™à¸à¹‡à¸ªà¸²à¸¡à¸²à¸£à¸–à¹à¸ªà¸”งถัดจาà¸à¸Ÿà¸´à¸¥à¸”์ได้นะ" _exportOrImport: allNotes: "โน้ตทั้งหมด" favoritedNotes: "บันทึà¸à¸—ี่ชื่นชà¸à¸š" @@ -2104,7 +2098,17 @@ _moderationLogTypes: updateUserNote: "à¸à¸±à¸›à¹€à¸”ตโน้ตà¸à¸²à¸£à¸à¸¥à¸±à¹ˆà¸™à¸à¸£à¸à¸‡à¹à¸¥à¹‰à¸§" deleteDriveFile: "ลบไฟล์à¸à¸à¸à¹à¸¥à¹‰à¸§" deleteNote: "ลบโน้ตà¸à¸à¸à¹à¸¥à¹‰à¸§" + createGlobalAnnouncement: "สร้างประà¸à¸²à¸¨à¸—ั่วโลà¸à¹à¸¥à¹‰à¸§" + createUserAnnouncement: "สร้างประà¸à¸²à¸¨à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¹à¸¥à¹‰à¸§" + updateGlobalAnnouncement: "à¸à¸±à¸›à¹€à¸”ตประà¸à¸²à¸¨à¸—ั่วโลà¸à¹à¸¥à¹‰à¸§" + updateUserAnnouncement: "à¸à¸±à¸›à¹€à¸”ตประà¸à¸²à¸¨à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¹à¸¥à¹‰à¸§" + deleteGlobalAnnouncement: "ลบประà¸à¸²à¸¨à¸—ั่วโลà¸à¸à¸à¸à¹à¸¥à¹‰à¸§" + deleteUserAnnouncement: "ลบประà¸à¸²à¸¨à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸à¸à¸à¹à¸¥à¹‰à¸§" resetPassword: "รีเซ็ตรหัสผ่าน" + suspendRemoteInstance: "à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥à¸–ูà¸à¸£à¸°à¸‡à¸±à¸š" + unsuspendRemoteInstance: "à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥à¹€à¸¥à¸´à¸à¸à¸²à¸£à¸£à¸°à¸‡à¸±à¸š" + markSensitiveDriveFile: "ทำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¹„ฟล์บà¸à¸à¸§à¹ˆà¸²à¸¥à¸°à¹€à¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™" + unmarkSensitiveDriveFile: "ยà¸à¹€à¸¥à¸´à¸à¸—ำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¹„ฟล์ว่าละเà¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™" resolveAbuseReport: "รายงานได้รับà¸à¸²à¸£à¹à¸à¹‰à¹„ขà¹à¸¥à¹‰à¸§" createInvitation: "สร้างคำเชิà¸" createAd: "สร้างโฆษณาà¹à¸¥à¹‰à¸§" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 1111c23091acc5e25fbe79fce055f57f8090cf25..90bee48a1fdbfd5937dc2ec3aafadf10eef90110 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -386,7 +386,6 @@ _theme: _sfx: note: "notlar" notification: "Bildirim" - chat: "Mesajlar" _2fa: renewTOTPCancel: "Hayır, teÅŸekkürler" _permissions: diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 09b3eba74595386f1cd99cead79aa36663244f12..8d843d67f8b410d8df78243d1b3b047740b976d6 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -1233,11 +1233,6 @@ _wordMute: muteWords: "Заглушені Ñлова" muteWordsDescription: "Ð Ð¾Ð·Ð´Ñ–Ð»ÐµÐ½Ð½Ñ ÐºÐ»ÑŽÑ‡Ð¾Ð²Ð¸Ñ… Ñлів пробілами Ð´Ð»Ñ \"І\" або з нової лінійки Ð´Ð»Ñ \"ÐБО\"" muteWordsDescription2: "Ð”Ð»Ñ Ð²Ð¸ÐºÐ¾Ñ€Ð¸ÑÑ‚Ð°Ð½Ð½Ñ RegEx, ключові Ñлова потрібно впиÑати поміж Ñлешів \"/\"." - softDescription: "Приховати запиÑи Ñкі відповідають критеріÑм зі Ñтрічки подій." - hardDescription: "Приховати запиÑи Ñкі відповідають критеріÑм зі Ñтрічки подій. Також приховані запиÑи не будуть додані до Ñтрічки подій навіть Ñкщо критерії буде змінено." - soft: "Ðœ'Ñко" - hard: "ЖорÑтко" - mutedNotes: "Заблоковані нотатки" _instanceMute: instanceMuteDescription2: "РозділÑйте новими Ñ€Ñдками" title: "Приховує нотатки з перелічених інÑтанÑів." @@ -1295,9 +1290,6 @@ _theme: infoFg: "ТекÑÑ‚ інформації" infoWarnBg: "Фон попередженнÑ" infoWarnFg: "ТекÑÑ‚ попередженнÑ" - cwBg: "Фон чутливого зміÑту" - cwFg: "ТекÑÑ‚ чутливого зміÑту" - cwHoverBg: "Фон чутливого зміÑту (при наведенні)" toastBg: "Фон повідомленнÑ" toastFg: "ТекÑÑ‚ повідомленнÑ" buttonBg: "Фон кнопки" @@ -1315,8 +1307,6 @@ _sfx: note: "Ðотатки" noteMy: "Мої нотатки" notification: "СповіщеннÑ" - chat: "Чати" - chatBg: "Чати (фон)" antenna: "Прийом антени" channel: "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ ÐºÐ°Ð½Ð°Ð»Ñƒ" _ago: diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml index 726333958b01b18652135f08a386af186139c6ea..3a9e6ec5e724ca17c3916aed3e05261090a43c8c 100644 --- a/locales/uz-UZ.yml +++ b/locales/uz-UZ.yml @@ -910,7 +910,6 @@ _theme: _sfx: note: "Qaydlar" notification: "Xabarnomalar" - chat: "Suhbat" _ago: minutesAgo: "{n} daqiqa oldin" hoursAgo: "{n} soat oldin" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 3b34e4711cad782b5f6a3660d3ca36e0a7436687..b8a77a92004360150a809a6594c1ead736204f40 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1404,11 +1404,6 @@ _wordMute: muteWords: "Ẩn từ ngữ" muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." muteWordsDescription2: "Bao quanh các từ khóa bằng dấu gạch chéo để sá» dụng cụm từ thông dụng." - softDescription: "Ẩn các tút phù hợp Ä‘iá»u kiện đã đặt khá»i bảng tin." - hardDescription: "Ngăn các tút đáp ứng các Ä‘iá»u kiện đã đặt xuất hiện trên bảng tin. LÆ°u ý, những tút nà y sẽ không được thêm và o bảng tin ngay cả khi các Ä‘iá»u kiện được thay đổi." - soft: "Yếu" - hard: "Mạnh" - mutedNotes: "Những tút đã ẩn" _instanceMute: instanceMuteDescription: "Thao tác nà y sẽ ẩn má»i tút/lượt đăng lại từ các máy chủ được liệt kê, bao gồm cả những tút dạng trả lá»i từ máy chủ bị ẩn." instanceMuteDescription2: "Tách bằng cách xuống dòng" @@ -1472,9 +1467,6 @@ _theme: infoFg: "Chữ thông tin" infoWarnBg: "Ná»n cảnh báo" infoWarnFg: "Chữ cảnh báo" - cwBg: "Ná»n nút ná»™i dung ẩn" - cwFg: "Chữ nút ná»™i dung ẩn" - cwHoverBg: "Ná»n nút ná»™i dung ẩn (Chạm)" toastBg: "Ná»n thông báo" toastFg: "Chữ thông báo" buttonBg: "Ná»n nút" @@ -1492,8 +1484,6 @@ _sfx: note: "Tút" noteMy: "Tút của tôi" notification: "Thông báo" - chat: "Trò chuyện" - chatBg: "Chat (Ná»n)" antenna: "Trạm phát sóng" channel: "Kênh" _ago: diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index a04697e480b8b2601b36cc99586ddb90f99236b7..dfc4ccb688ec4068a3b23411e2e9a7183a6cad20 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1126,6 +1126,8 @@ edited: "已编辑" notificationRecieveConfig: "通知接收设置" mutualFollow: "互相关注" fileAttachedOnly: "ä»…é™åª’体" +showRepliesToOthersInTimeline: "在时间线上显示给其他人的回å¤" +hideRepliesToOthersInTimeline: "在时间线上éšè—给其他人的回å¤" _announcement: forExistingUsers: "ä»…é™çŽ°æœ‰ç”¨æˆ·" forExistingUsersDescription: "è‹¥å¯ç”¨ï¼Œè¯¥å…¬å‘Šå°†ä»…对创建æ¤å…¬å‘Šæ—¶å˜åœ¨çš„用户å¯è§ã€‚ 如果ç¦ç”¨ï¼Œåˆ™åœ¨åˆ›å»ºæ¤å…¬å‘ŠåŽæ³¨å†Œçš„用户也å¯ä»¥çœ‹åˆ°è¯¥å…¬å‘Šã€‚" @@ -1456,7 +1458,6 @@ _role: gtlAvailable: "查看全局时间线" ltlAvailable: "查看本地时间线" canPublicNote: "å…许公开å‘帖" - canEditNote: "编辑帖å" canInvite: "å‘放æœåŠ¡å™¨é‚€è¯·ç " inviteLimit: "å¯å‘行邀请ç çš„æ•°é‡" inviteLimitCycle: "邀请ç çš„å‘行间隔" @@ -1609,11 +1610,6 @@ _wordMute: muteWords: "ç¦ç”¨è¯" muteWordsDescription: "AND æ¡ä»¶ç”¨ç©ºæ ¼åˆ†éš”,OR æ¡ä»¶ç”¨æ¢è¡Œç¬¦åˆ†éš”。" muteWordsDescription2: "æ£åˆ™è¡¨è¾¾å¼ç”¨æ–œçº¿åŒ…裹" - softDescription: "éšè—时间线ä¸æŒ‡å®šæ¡ä»¶çš„帖å。" - hardDescription: "防æ¢å°†å…·æœ‰æŒ‡å®šæ¡ä»¶çš„帖åæ·»åŠ åˆ°æ—¶é—´çº¿ã€‚ å³ä½¿æ‚¨æ›´æ”¹æ¡ä»¶ï¼Œæœªæ·»åŠ 的帖文也会被排除在外。" - soft: "软å±è”½" - hard: "硬å±è”½" - mutedNotes: "被å±è”½çš„帖å" _instanceMute: instanceMuteDescription: "å±è”½æœåŠ¡å™¨ä¸çš„所有帖å和转帖,包括这些æœåŠ¡å™¨ä¸Šçš„用户回å¤ã€‚" instanceMuteDescription2: "一行一个" @@ -1677,9 +1673,6 @@ _theme: infoFg: "ä¿¡æ¯æ–‡æœ¬" infoWarnBg: "è¦å‘ŠèƒŒæ™¯" infoWarnFg: "è¦å‘Šæ–‡æœ¬" - cwBg: "éšè—内容按钮背景" - cwFg: "éšè—内容按钮文本" - cwHoverBg: "éšè—内容按钮背景(悬åœï¼‰" toastBg: "Toast 通知背景" toastFg: "Toast 通知文本" buttonBg: "按钮背景" @@ -1697,8 +1690,6 @@ _sfx: note: "帖å" noteMy: "我的帖å" notification: "通知" - chat: "èŠå¤©" - chatBg: "èŠå¤©èƒŒæ™¯" antenna: "天线接收" channel: "频é“通知" _ago: diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index c0bf1f7d1bc39bda542e6d002467c94db8d43365..e715cae547bc60602092727a28b4b07409ce70c7 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -156,7 +156,7 @@ emojiUrl: "表情符號 URL" addEmoji: "新增表情符號" settingGuide: "推薦è¨å®š" cacheRemoteFiles: "å¿«å–é 端檔案" -cacheRemoteFilesDescription: "啟用æ¤è¨å®šå¾Œï¼Œé 端檔案會被快å–在本伺æœå™¨çš„儲å˜ç©ºé–“ä¸ã€‚雖然顯示圖片會變快,但會消耗較多伺æœå™¨çš„儲å˜ç©ºé–“。至於è¦å¿«å–é 端使用者到什麼程度,是ä¾ç…§è§’色的雲端硬碟容é‡è€Œå®šã€‚當超éŽé€™å€‹é™åˆ¶æ™‚,從較舊的檔案開始自快å–ä¸åˆªé™¤ä¸¦æ”¹ç‚ºé€£çµã€‚關閉這個è¨å®šæ™‚,é 端檔案從一開始就ç¶æŒé€£çµçš„æ–¹å¼ï¼Œä½†å»ºè°å°‡ default.yml çš„ proxyRemoteFiles è¨ç‚º true,以便產生圖片的縮圖並ä¿è·ä½¿ç”¨è€…çš„éš±ç§ï¼Œã€‚" +cacheRemoteFilesDescription: "啟用æ¤è¨å®šå¾Œï¼Œé 端檔案會被快å–在本伺æœå™¨çš„儲å˜ç©ºé–“ä¸ã€‚雖然顯示圖片會變快,但會消耗較多伺æœå™¨çš„儲å˜ç©ºé–“。至於è¦å¿«å–é 端使用者到什麼程度,是ä¾ç…§è§’色的雲端硬碟容é‡è€Œå®šã€‚當超éŽé€™å€‹é™åˆ¶æ™‚,從較舊的檔案開始自快å–ä¸åˆªé™¤ä¸¦æ”¹ç‚ºé€£çµã€‚關閉這個è¨å®šæ™‚,é 端檔案從一開始就ç¶æŒé€£çµçš„æ–¹å¼ï¼Œä½†å»ºè°å°‡ default.yml çš„ proxyRemoteFiles è¨ç‚º true,以便產生圖片的縮圖並ä¿è·ä½¿ç”¨è€…çš„éš±ç§ã€‚" youCanCleanRemoteFilesCache: "按檔案管ç†çš„🗑ï¸æŒ‰éˆ•ï¼Œå¯å°‡å¿«å–全部刪除。" cacheRemoteSensitiveFiles: "å¿«å–é 端的æ•æ„Ÿæª”案" cacheRemoteSensitiveFilesDescription: "è‹¥åœç”¨é€™å€‹è¨å®šï¼Œå‰‡ä¸æœƒå¿«å–é 端的æ•æ„Ÿæª”案,而是直接連çµã€‚" @@ -1123,7 +1123,18 @@ authenticationRequiredToContinue: "請於繼續å‰å®Œæˆé©—è‰" dateAndTime: "日期與時間" showRenotes: "顯示轉發貼文" edited: "已編輯" +notificationRecieveConfig: "接å—通知的è¨å®š" mutualFollow: "互相追隨" +fileAttachedOnly: "包å«é™„件" +showRepliesToOthersInTimeline: "在時間軸上顯示給其他人的回覆" +hideRepliesToOthersInTimeline: "在時間軸上隱è—給其他人的回覆" +externalServices: "外部æœå‹™" +impressum: "營é‹è€…資訊" +impressumUrl: "營é‹è€…資訊網å€" +impressumDescription: "在德國與部份地å€å¿…é ˆè¦æ˜Žç¢ºé¡¯ç¤ºç‡Ÿé‹è€…資訊。" +privacyPolicy: "éš±ç§æ”¿ç–" +privacyPolicyUrl: "éš±ç§æ”¿ç–網å€" +tosAndPrivacyPolicy: "æœå‹™æ¢æ¬¾å’Œéš±ç§æ”¿ç–" _announcement: forExistingUsers: "僅é™æ—¢æœ‰çš„使用者" forExistingUsersDescription: "啟用代表僅å‘ç¾å˜ä½¿ç”¨è€…顯示;åœç”¨ä»£è¡¨å¼µè²¼å¾Œè¨»å†Šçš„新使用者也會看到。" @@ -1454,7 +1465,6 @@ _role: gtlAvailable: "ç€è¦½å…¨åŸŸæ™‚間軸" ltlAvailable: "ç€è¦½æœ¬åœ°æ™‚間軸" canPublicNote: "å…許公開貼文" - canEditNote: "å…許編輯貼文" canInvite: "發行實例邀請碼" inviteLimit: "å¯å»ºç«‹é‚€è«‹ç¢¼çš„數é‡" inviteLimitCycle: "邀請碼的發放間隔" @@ -1474,6 +1484,7 @@ _role: descriptionOfRateLimitFactor: "值越å°é™åˆ¶è¶Šå°‘,值越大é™åˆ¶è¶Šå¤šã€‚" canHideAds: "ä¸é¡¯ç¤ºå»£å‘Š" canSearchNotes: "å¯å¦æœå°‹è²¼æ–‡" + canUseTranslator: "使用翻è¯åŠŸèƒ½" _condition: isLocal: "本地使用者" isRemote: "é 端使用者" @@ -1522,6 +1533,10 @@ _ad: reduceFrequencyOfThisAd: "é™ä½Žæ¤å»£å‘Šçš„é »çŽ‡ " hide: "éš±è—" timezoneinfo: "星期幾是由伺æœå™¨çš„時å€æŒ‡å®šçš„。" + adsSettings: "廣告投放è¨å®š" + notesPerOneAd: "å³æ™‚æ›´æ–°ä¸æŠ•æ”¾å»£å‘Šçš„間隔(貼文數)" + setZeroToDisable: "è¨ç‚º 0 則在å³æ™‚更新時ä¸æŠ•æ”¾å»£å‘Š" + adsTooClose: "由於廣告投放的間隔極çŸï¼Œå¯èƒ½æœƒåš´é‡å½±éŸ¿ä½¿ç”¨è€…體驗。" _forgotPassword: enterEmail: "請輸入您的帳戶註冊的電å郵件地å€ã€‚ 密碼é‡ç½®é€£çµå°‡è¢«ç™¼é€åˆ°è©²é›»å郵件地å€ã€‚" ifNoEmail: "如果您還沒有註冊您的電å郵件地å€ï¼Œè«‹è¯ç¹«ç®¡ç†å“¡ã€‚ " @@ -1607,11 +1622,6 @@ _wordMute: muteWords: "åŠ å…¥éœéŸ³æ–‡å—" muteWordsDescription: "ç©ºæ ¼ä»£è¡¨ã€Œä»¥åŠã€ï¼ˆAND),æ›è¡Œä»£è¡¨ã€Œæˆ–者ã€ï¼ˆOR)。" muteWordsDescription2: "用斜線包åœé—œéµå—代表æ£è¦è¡¨é”å¼ã€‚" - softDescription: "éš±è—時間軸ä¸ç¬¦åˆç‰¹å®šæ¢ä»¶çš„貼文。" - hardDescription: "符åˆç‰¹å®šæ¢ä»¶çš„貼文將ä¸æœƒæ–°å¢žè‡³æ™‚間軸。 å³ä½¿æ‚¨æ›´æ”¹æ¢ä»¶ï¼Œæœªè¢«æ–°å¢žçš„貼文也會被排除在外。" - soft: "軟性éœéŸ³" - hard: "硬性éœéŸ³" - mutedNotes: "å·²éœéŸ³çš„貼文" _instanceMute: instanceMuteDescription: "包括å°è¢«éœéŸ³å¯¦ä¾‹ä¸Šçš„使用者的回覆,被è¨å®šçš„實例上所有貼文åŠè½‰ç™¼éƒ½æœƒè¢«éœéŸ³ã€‚" instanceMuteDescription2: "è¨å®šæ™‚以æ›è¡Œé€²è¡Œåˆ†éš”" @@ -1675,9 +1685,6 @@ _theme: infoFg: "資訊內容" infoWarnBg: "è¦å‘ŠèƒŒæ™¯" infoWarnFg: "è¦å‘Šæ–‡å—" - cwBg: "éš±è—內容按鈕背景" - cwFg: "éš±è—內容按鈕文å—" - cwHoverBg: "éš±è—內容按鈕背景(懸浮)" toastBg: "通知背景" toastFg: "通知文本" buttonBg: "按鈕背景" @@ -1695,8 +1702,6 @@ _sfx: note: "貼文" noteMy: "我的貼文" notification: "通知" - chat: "èŠå¤©" - chatBg: "èŠå¤©èƒŒæ™¯" antenna: "天線接收" channel: "é »é“通知" _ago: diff --git a/package.json b/package.json index 2259ede6d55ef8ad60134b48ad0cb5cfad6c024a..e94852616e9db99c23235d2ca91c24165c3ac7a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2023.9.3", + "version": "2023.10.0", "codename": "nasubi", "repository": { "type": "git", @@ -47,15 +47,15 @@ "cssnano": "6.0.1", "js-yaml": "4.1.0", "postcss": "8.4.31", - "terser": "5.20.0", + "terser": "5.21.0", "typescript": "5.2.2" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "6.7.3", - "@typescript-eslint/parser": "6.7.3", + "@typescript-eslint/eslint-plugin": "6.7.5", + "@typescript-eslint/parser": "6.7.5", "cross-env": "7.0.3", "cypress": "13.3.0", - "eslint": "8.50.0", + "eslint": "8.51.0", "start-server-and-test": "2.0.1" }, "optionalDependencies": { diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index 6b1afec734923c16cc568f5cbac13a5238c5bf79..97d777c86287093f8b1f7ccaa73be9dbbd473701 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -216,4 +216,6 @@ module.exports = { maxWorkers: 1, // Make it use worker (that can be killed and restarted) logHeapUsage: true, // To debug when out-of-memory happens on CI workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB) + + maxConcurrency: 32, }; diff --git a/packages/backend/migration/1696003580220-AddSomeUrls.js b/packages/backend/migration/1696003580220-AddSomeUrls.js new file mode 100644 index 0000000000000000000000000000000000000000..683aa5eeed579521989e66bbb0536ae0e03d99fa --- /dev/null +++ b/packages/backend/migration/1696003580220-AddSomeUrls.js @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddSomeUrls1696003580220 { + name = 'AddSomeUrls1696003580220' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "impressumUrl" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "privacyPolicyUrl" character varying(1024)`); + } + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "impressumUrl"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "privacyPolicyUrl"`); + } +} diff --git a/packages/backend/migration/1696222183852-withReplies.js b/packages/backend/migration/1696222183852-withReplies.js new file mode 100644 index 0000000000000000000000000000000000000000..9f65d5f6a1629a142daa40b9bee4b04ff6a093ff --- /dev/null +++ b/packages/backend/migration/1696222183852-withReplies.js @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class WithReplies1696222183852 { + name = 'WithReplies1696222183852' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "following" ADD "withReplies" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "user_list_joining" ADD "withReplies" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`); + await queryRunner.query(`ALTER TABLE "user_list_joining" DROP COLUMN "withReplies"`); + await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "withReplies"`); + } +} diff --git a/packages/backend/migration/1696323464251-user-list-membership.js b/packages/backend/migration/1696323464251-user-list-membership.js new file mode 100644 index 0000000000000000000000000000000000000000..7534040c4c9bce481af8cf217b7452724d7a1e24 --- /dev/null +++ b/packages/backend/migration/1696323464251-user-list-membership.js @@ -0,0 +1,11 @@ +export class UserListMembership1696323464251 { + name = 'UserListMembership1696323464251' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_joining" RENAME TO "user_list_membership"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_membership" RENAME TO "user_list_joining"`); + } +} diff --git a/packages/backend/migration/1696331570827-hibernation.js b/packages/backend/migration/1696331570827-hibernation.js new file mode 100644 index 0000000000000000000000000000000000000000..119d35913f7b8bc19b93f35fb66a1a9e37013478 --- /dev/null +++ b/packages/backend/migration/1696331570827-hibernation.js @@ -0,0 +1,17 @@ +export class Hibernation1696331570827 { + name = 'Hibernation1696331570827' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`); + await queryRunner.query(`ALTER TABLE "user" ADD "isHibernated" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "following" ADD "isFollowerHibernated" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`CREATE INDEX "IDX_ce62b50d882d4e9dee10ad0d2f" ON "following" ("followeeId", "followerHost", "isFollowerHibernated") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_ce62b50d882d4e9dee10ad0d2f"`); + await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "isFollowerHibernated"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isHibernated"`); + await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `); + } +} diff --git a/packages/backend/migration/1696332072038-clean.js b/packages/backend/migration/1696332072038-clean.js new file mode 100644 index 0000000000000000000000000000000000000000..97dba655f46c441179d9b5e1270d494d24703cc0 --- /dev/null +++ b/packages/backend/migration/1696332072038-clean.js @@ -0,0 +1,33 @@ +export class Clean1696332072038 { + name = 'Clean1696332072038' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_d844bfc6f3f523a05189076efaa"`); + await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_605472305f26818cc93d1baaa74"`); + await queryRunner.query(`DROP INDEX "public"."IDX_d844bfc6f3f523a05189076efa"`); + await queryRunner.query(`DROP INDEX "public"."IDX_605472305f26818cc93d1baaa7"`); + await queryRunner.query(`DROP INDEX "public"."IDX_90f7da835e4c10aca6853621e1"`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }'`); + await queryRunner.query(`COMMENT ON COLUMN "user_list_membership"."createdAt" IS 'The created date of the UserListMembership.'`); + await queryRunner.query(`CREATE INDEX "IDX_021015e6683570ae9f6b0c62be" ON "user_list_membership" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_cddcaf418dc4d392ecfcca842a" ON "user_list_membership" ("userListId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e4f3094c43f2d665e6030b0337" ON "user_list_membership" ("userId", "userListId") `); + await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_021015e6683570ae9f6b0c62bee" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_cddcaf418dc4d392ecfcca842a7" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_cddcaf418dc4d392ecfcca842a7"`); + await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_021015e6683570ae9f6b0c62bee"`); + await queryRunner.query(`DROP INDEX "public"."IDX_e4f3094c43f2d665e6030b0337"`); + await queryRunner.query(`DROP INDEX "public"."IDX_cddcaf418dc4d392ecfcca842a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_021015e6683570ae9f6b0c62be"`); + await queryRunner.query(`COMMENT ON COLUMN "user_list_membership"."createdAt" IS 'The created date of the UserListJoining.'`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{admin,administrator,root,system,maintainer,host,mod,moderator,owner,superuser,staff,auth,i,me,everyone,all,mention,mentions,example,user,users,account,accounts,official,help,helps,support,supports,info,information,informations,announce,announces,announcement,announcements,notice,notification,notifications,dev,developer,developers,tech,misskey}'`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_90f7da835e4c10aca6853621e1" ON "user_list_membership" ("userId", "userListId") `); + await queryRunner.query(`CREATE INDEX "IDX_605472305f26818cc93d1baaa7" ON "user_list_membership" ("userListId") `); + await queryRunner.query(`CREATE INDEX "IDX_d844bfc6f3f523a05189076efa" ON "user_list_membership" ("userId") `); + await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_605472305f26818cc93d1baaa74" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_d844bfc6f3f523a05189076efaa" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } +} diff --git a/packages/backend/migration/1696373953614-meta-cache-settings.js b/packages/backend/migration/1696373953614-meta-cache-settings.js new file mode 100644 index 0000000000000000000000000000000000000000..f994b76ef26325dff6b502282b757a38a93ac0ac --- /dev/null +++ b/packages/backend/migration/1696373953614-meta-cache-settings.js @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class MetaCacheSettings1696373953614 { + name = 'MetaCacheSettings1696373953614' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "perLocalUserUserTimelineCacheMax" integer NOT NULL DEFAULT '300'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "perRemoteUserUserTimelineCacheMax" integer NOT NULL DEFAULT '100'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "perUserHomeTimelineCacheMax" integer NOT NULL DEFAULT '300'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "perUserListTimelineCacheMax" integer NOT NULL DEFAULT '300'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserListTimelineCacheMax"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserHomeTimelineCacheMax"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perRemoteUserUserTimelineCacheMax"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perLocalUserUserTimelineCacheMax"`); + } +} diff --git a/packages/backend/migration/1696388600237-revert-note-edit.js b/packages/backend/migration/1696388600237-revert-note-edit.js new file mode 100644 index 0000000000000000000000000000000000000000..83bc552c35a7aa91b88363cfdb3bcc23a71b5c6f --- /dev/null +++ b/packages/backend/migration/1696388600237-revert-note-edit.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RevertNoteEdit1696388600237 { + name = 'RevertNoteEdit1696388600237' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`); + } +} diff --git a/packages/backend/migration/1696405744672-clean-up.js b/packages/backend/migration/1696405744672-clean-up.js new file mode 100644 index 0000000000000000000000000000000000000000..5ec89b08f4fd5325700ebcee0cb93cf0ad6c3c06 --- /dev/null +++ b/packages/backend/migration/1696405744672-clean-up.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class CleanUp1696405744672 { + name = 'CleanUp1696405744672' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_e7c0567f5261063592f022e9b5"`); + await queryRunner.query(`DROP INDEX "public"."IDX_25dfc71b0369b003a4cd434d0b"`); + } + + async down(queryRunner) { + await queryRunner.query(`CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes") `); + await queryRunner.query(`CREATE INDEX "IDX_e7c0567f5261063592f022e9b5" ON "note" ("createdAt") `); + } +} diff --git a/packages/backend/migration/1696569742153-clean-up.js b/packages/backend/migration/1696569742153-clean-up.js new file mode 100644 index 0000000000000000000000000000000000000000..de48fab5aa84dfb5bd7b93277e526bff65fb1b70 --- /dev/null +++ b/packages/backend/migration/1696569742153-clean-up.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class CleanUp1696569742153 { + name = 'CleanUp1696569742153' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_01f4581f114e0ebd2bbb876f0b"`); + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "score"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "score" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt") `); + } +} diff --git a/packages/backend/migration/1696581429196-clean-up.js b/packages/backend/migration/1696581429196-clean-up.js new file mode 100644 index 0000000000000000000000000000000000000000..da69b4e9de05f83eed88c77b6ee88160e4ef3ea0 --- /dev/null +++ b/packages/backend/migration/1696581429196-clean-up.js @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class CleanUp1696581429196 { + name = 'CleanUp1696581429196' + + async up(queryRunner) { + await queryRunner.query(`DROP TABLE IF EXISTS "muted_note"`); + } + + async down(queryRunner) { + } +} diff --git a/packages/backend/migration/1696743032098-AdsOnStream.js b/packages/backend/migration/1696743032098-AdsOnStream.js new file mode 100644 index 0000000000000000000000000000000000000000..c86ee84883283081926f7fba4c6a869d046845f1 --- /dev/null +++ b/packages/backend/migration/1696743032098-AdsOnStream.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AdsOnStream1696743032098 { + name = 'AdsOnStream1696743032098' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "notesPerOneAd" integer NOT NULL DEFAULT '0'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "notesPerOneAd"`); + } +} diff --git a/packages/backend/migration/1696807733453-userListUserId.js b/packages/backend/migration/1696807733453-userListUserId.js new file mode 100644 index 0000000000000000000000000000000000000000..ab2ba07fb5f8011d7f62e04e498b35a10c4d066b --- /dev/null +++ b/packages/backend/migration/1696807733453-userListUserId.js @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class UserListUserId1696807733453 { + name = 'UserListUserId1696807733453' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_membership" ADD "userListUserId" character varying(32) NOT NULL DEFAULT ''`); + const memberships = await queryRunner.query(`SELECT "id", "userListId" FROM "user_list_membership"`); + for(let i = 0; i < memberships.length; i++) { + const userList = await queryRunner.query(`SELECT "userId" FROM "user_list" WHERE "id" = $1`, [memberships[i].userListId]); + await queryRunner.query(`UPDATE "user_list_membership" SET "userListUserId" = $1 WHERE "id" = $2`, [userList[0].userId, memberships[i].id]); + } + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_membership" DROP COLUMN "userListUserId"`); + } +} diff --git a/packages/backend/migration/1696808725134-userListUserId-2.js b/packages/backend/migration/1696808725134-userListUserId-2.js new file mode 100644 index 0000000000000000000000000000000000000000..5bcb5aedc2fbf4196934ec0b76d75384563b9ba4 --- /dev/null +++ b/packages/backend/migration/1696808725134-userListUserId-2.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class UserListUserId21696808725134 { + name = 'UserListUserId21696808725134' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_membership" ALTER COLUMN "userListUserId" DROP DEFAULT`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_membership" ALTER COLUMN "userListUserId" SET DEFAULT ''`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 2fbf9de8a6d988c4a8460a1bf6896c9aa56a3e67..bc24b4938cac568d953b2ccb87778838ce320f57 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -71,14 +71,14 @@ "@fastify/multipart": "8.0.0", "@fastify/static": "6.11.2", "@fastify/view": "8.2.0", - "@nestjs/common": "10.2.6", - "@nestjs/core": "10.2.6", - "@nestjs/testing": "10.2.6", + "@nestjs/common": "10.2.7", + "@nestjs/core": "10.2.7", + "@nestjs/testing": "10.2.7", "@peertube/http-signature": "1.7.0", "@simplewebauthn/server": "8.2.0", "@sinonjs/fake-timers": "11.1.0", "@swc/cli": "0.1.62", - "@swc/core": "1.3.90", + "@swc/core": "1.3.92", "accepts": "1.3.8", "ajv": "8.12.0", "archiver": "6.0.1", @@ -86,7 +86,7 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "4.11.4", + "bullmq": "4.12.3", "cacheable-lookup": "7.0.0", "cbor": "9.0.1", "chalk": "5.3.0", @@ -124,13 +124,13 @@ "nanoid": "5.0.1", "nested-property": "4.0.0", "node-fetch": "3.3.2", - "nodemailer": "6.9.5", + "nodemailer": "6.9.6", "nsfwjs": "2.4.2", "oauth": "0.10.0", "oauth2orize": "1.11.1", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.1.4", + "otpauth": "9.1.5", "parse5": "7.1.2", "pg": "8.11.3", "pkce-challenge": "4.0.1", @@ -155,7 +155,7 @@ "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "summaly": "github:misskey-dev/summaly", - "systeminformation": "5.21.9", + "systeminformation": "5.21.11", "tinycolor2": "1.6.0", "tmp": "0.2.1", "tsc-alias": "1.8.8", @@ -189,13 +189,13 @@ "@types/jsrsasign": "10.5.9", "@types/mime-types": "2.1.2", "@types/ms": "0.7.32", - "@types/node": "20.7.1", + "@types/node": "20.8.4", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.11", "@types/oauth": "0.9.2", "@types/oauth2orize": "1.11.1", "@types/oauth2orize-pkce": "0.1.0", - "@types/pg": "8.10.3", + "@types/pg": "8.10.4", "@types/pug": "2.0.7", "@types/punycode": "2.1.0", "@types/qrcode": "1.5.2", @@ -212,11 +212,11 @@ "@types/vary": "1.1.1", "@types/web-push": "3.6.1", "@types/ws": "8.5.6", - "@typescript-eslint/eslint-plugin": "6.7.3", - "@typescript-eslint/parser": "6.7.3", + "@typescript-eslint/eslint-plugin": "6.7.5", + "@typescript-eslint/parser": "6.7.5", "aws-sdk-client-mock": "3.0.0", "cross-env": "7.0.3", - "eslint": "8.50.0", + "eslint": "8.51.0", "eslint-plugin-import": "2.28.1", "execa": "8.0.1", "jest": "29.7.0", diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 9f1ee9fcaa3f53bf80672d465c6469df0368982e..3e9d19f82598d71c9e161aff176d76e062089379 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -70,11 +70,19 @@ const $redisForSub: Provider = { inject: [DI.config], }; +const $redisForTimelines: Provider = { + provide: DI.redisForTimelines, + useFactory: (config: Config) => { + return new Redis.Redis(config.redisForTimelines); + }, + inject: [DI.config], +}; + @Global() @Module({ imports: [RepositoryModule], - providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub], - exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, RepositoryModule], + providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines], + exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule], }) export class GlobalModule implements OnApplicationShutdown { constructor( @@ -82,6 +90,7 @@ export class GlobalModule implements OnApplicationShutdown { @Inject(DI.redis) private redisClient: Redis.Redis, @Inject(DI.redisForPub) private redisForPub: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis, + @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, ) {} public async dispose(): Promise<void> { @@ -98,6 +107,7 @@ export class GlobalModule implements OnApplicationShutdown { this.redisClient.disconnect(), this.redisForPub.disconnect(), this.redisForSub.disconnect(), + this.redisForTimelines.disconnect(), ]); } diff --git a/packages/backend/src/boot/common.ts b/packages/backend/src/boot/common.ts index 4783a2b2dae593df7162463f3d1a942d4c0a9ac9..df10ab1e3db416d334e803fab1bdcbb6a771c262 100644 --- a/packages/backend/src/boot/common.ts +++ b/packages/backend/src/boot/common.ts @@ -17,7 +17,6 @@ export async function server() { const app = await NestFactory.createApplicationContext(MainModule, { logger: new NestLogger(), }); - app.enableShutdownHooks(); const serverService = app.get(ServerService); await serverService.launch(); @@ -35,7 +34,6 @@ export async function jobQueue() { const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, { logger: new NestLogger(), }); - jobQueue.enableShutdownHooks(); jobQueue.get(QueueProcessorService).start(); jobQueue.get(ChartManagementService).start(); diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index f89879d535192a035ca13bba135f598e1bb552a4..ef59a80950fb9da87c9db220391afa94138755a0 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -47,6 +47,7 @@ type Source = { redis: RedisOptionsSource; redisForPubsub?: RedisOptionsSource; redisForJobQueue?: RedisOptionsSource; + redisForTimelines?: RedisOptionsSource; meilisearch?: { host: string; port: string; @@ -161,6 +162,7 @@ export type Config = { redis: RedisOptions & RedisOptionsSource; redisForPubsub: RedisOptions & RedisOptionsSource; redisForJobQueue: RedisOptions & RedisOptionsSource; + redisForTimelines: RedisOptions & RedisOptionsSource; perChannelMaxNoteCacheCount: number; perUserNotificationsMaxCount: number; deactivateAntennaThreshold: number; @@ -227,6 +229,7 @@ export function loadConfig(): Config { redis, redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis, redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis, + redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis, id: config.id, proxy: config.proxy, proxySmtp: config.proxySmtp, diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index ec1d013922b78d1b64857fe8affe045e1167aaec..db64f42754cc7713e8d8245a3d90e95463231f3e 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/_.js'; +import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; import { IdService } from '@/core/IdService.js'; @@ -42,8 +42,8 @@ export class AccountMoveService { @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, @@ -215,40 +215,41 @@ export class AccountMoveService { @bindThis public async updateLists(src: ThinUser, dst: MiUser): Promise<void> { // Return if there is no list to be updated. - const oldJoinings = await this.userListJoiningsRepository.find({ + const oldMemberships = await this.userListMembershipsRepository.find({ where: { userId: src.id, }, }); - if (oldJoinings.length === 0) return; + if (oldMemberships.length === 0) return; - const existingUserListIds = await this.userListJoiningsRepository.find({ + const existingUserListIds = await this.userListMembershipsRepository.find({ where: { userId: dst.id, }, - }).then(joinings => joinings.map(joining => joining.userListId)); + }).then(memberships => memberships.map(membership => membership.userListId)); - const newJoinings: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map(); + const newMemberships: Map<string, { createdAt: Date; userId: string; userListId: string; userListUserId: string; }> = new Map(); // é‡è¤‡ã—ãªã„よã†ã«IDã‚’ç”Ÿæˆ const genId = (): string => { let id: string; do { id = this.idService.genId(); - } while (newJoinings.has(id)); + } while (newMemberships.has(id)); return id; }; - for (const joining of oldJoinings) { - if (existingUserListIds.includes(joining.userListId)) continue; // skip if dst exists in this user's list - newJoinings.set(genId(), { + for (const membership of oldMemberships) { + if (existingUserListIds.includes(membership.userListId)) continue; // skip if dst exists in this user's list + newMemberships.set(genId(), { createdAt: new Date(), userId: dst.id, - userListId: joining.userListId, + userListId: membership.userListId, + userListUserId: membership.userListUserId, }); } - const arrayToInsert = Array.from(newJoinings.entries()).map(entry => ({ ...entry[1], id: entry[0] })); - await this.userListJoiningsRepository.insert(arrayToInsert); + const arrayToInsert = Array.from(newMemberships.entries()).map(entry => ({ ...entry[1], id: entry[0] })); + await this.userListMembershipsRepository.insert(arrayToInsert); // Have the proxy account follow the new account in the same way as UserListService.push if (this.userEntityService.isRemoteUser(dst)) { diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts index ddacc0936f1c270a560cc92bad183804a0e7a1d7..a5330db53f5950ed34270d801f5565bd6793fea0 100644 --- a/packages/backend/src/core/AnnouncementService.ts +++ b/packages/backend/src/core/AnnouncementService.ts @@ -158,9 +158,13 @@ export class AnnouncementService { if (moderator) { if (announcement.userId) { + const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId }); this.moderationLogService.log(moderator, 'deleteUserAnnouncement', { announcementId: announcement.id, announcement: announcement, + userId: announcement.userId, + userUsername: user.username, + userHost: user.host, }); } else { this.moderationLogService.log(moderator, 'deleteGlobalAnnouncement', { diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index d9f27b8c63061981f4b6e7a9b7acfe396030abb9..ca7624b1d4fbeb18089441f1e230229d4bda5af0 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -12,10 +12,11 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import * as Acct from '@/misc/acct.js'; import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; -import type { AntennasRepository, UserListJoiningsRepository } from '@/models/_.js'; +import type { AntennasRepository, UserListMembershipsRepository } from '@/models/_.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() @@ -24,8 +25,8 @@ export class AntennaService implements OnApplicationShutdown { private antennas: MiAntenna[]; constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis, @@ -33,11 +34,12 @@ export class AntennaService implements OnApplicationShutdown { @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private utilityService: UtilityService, private globalEventService: GlobalEventService, + private redisTimelineService: RedisTimelineService, ) { this.antennasFetched = false; this.antennas = []; @@ -81,15 +83,10 @@ export class AntennaService implements OnApplicationShutdown { 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(); + const redisPipeline = this.redisForTimelines.pipeline(); for (const antenna of matchedAntennas) { - redisPipeline.xadd( - `antennaTimeline:${antenna.id}`, - 'MAXLEN', '~', '200', - '*', - 'note', note.id); - + this.redisTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline); this.globalEventService.publishAntennaStream(antenna.id, 'note', note); } @@ -108,7 +105,7 @@ export class AntennaService implements OnApplicationShutdown { if (antenna.src === 'home') { // TODO } else if (antenna.src === 'list') { - const listUsers = (await this.userListJoiningsRepository.findBy({ + const listUsers = (await this.userListMembershipsRepository.findBy({ userListId: antenna.userListId!, })).map(x => x.userId); diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 561979c4bf142f85cd09f316f2e53d4cbd4b5ff7..22c510cc378d3fe692c27ca43df6eaedecc418e9 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; @@ -25,7 +25,7 @@ export class CacheService implements OnApplicationShutdown { public userBlockingCache: RedisKVCache<Set<string>>; public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被ã€Blockã‚ャッシュ public renoteMutingsCache: RedisKVCache<Set<string>>; - public userFollowingsCache: RedisKVCache<Set<string>>; + public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>; public userFollowingChannelsCache: RedisKVCache<Set<string>>; constructor( @@ -136,12 +136,18 @@ export class CacheService implements OnApplicationShutdown { fromRedisConverter: (value) => new Set(JSON.parse(value)), }); - this.userFollowingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowings', { + this.userFollowingsCache = new RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>(this.redisClient, 'userFollowings', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60, // 1m - fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))), - toRedisConverter: (value) => JSON.stringify(Array.from(value)), - fromRedisConverter: (value) => new Set(JSON.parse(value)), + fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId', 'withReplies'] }).then(xs => { + const obj: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {}; + for (const x of xs) { + obj[x.followeeId] = { withReplies: x.withReplies }; + } + return obj; + }), + toRedisConverter: (value) => JSON.stringify(value), + fromRedisConverter: (value) => JSON.parse(value), }); this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', { @@ -188,6 +194,7 @@ export class CacheService implements OnApplicationShutdown { if (follower) follower.followingCount++; const followee = this.userByIdCache.get(body.followeeId); if (followee) followee.followersCount++; + this.userFollowingsCache.delete(body.followerId); break; } default: diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 78333e70a5e0f90fe3e44fadcbec9777f8079120..0dc025d998cea05db453e701da898067d3a03de7 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -46,6 +46,7 @@ import { SignupService } from './SignupService.js'; import { WebAuthnService } from './WebAuthnService.js'; import { UserBlockingService } from './UserBlockingService.js'; import { CacheService } from './CacheService.js'; +import { UserService } from './UserService.js'; import { UserFollowingService } from './UserFollowingService.js'; import { UserKeypairService } from './UserKeypairService.js'; import { UserListService } from './UserListService.js'; @@ -59,6 +60,8 @@ import { UtilityService } from './UtilityService.js'; import { FileInfoService } from './FileInfoService.js'; import { SearchService } from './SearchService.js'; import { ClipService } from './ClipService.js'; +import { FeaturedService } from './FeaturedService.js'; +import { RedisTimelineService } from './RedisTimelineService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js'; import FederationChart from './chart/charts/federation.js'; import NotesChart from './chart/charts/notes.js'; @@ -173,6 +176,7 @@ const $SignupService: Provider = { provide: 'SignupService', useExisting: Signup const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService }; const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService }; const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService }; +const $UserService: Provider = { provide: 'UserService', useExisting: UserService }; const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService }; const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService }; const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService }; @@ -185,6 +189,8 @@ const $UtilityService: Provider = { provide: 'UtilityService', useExisting: Util const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService }; const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService }; +const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService }; +const $RedisTimelineService: Provider = { provide: 'RedisTimelineService', useExisting: RedisTimelineService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; @@ -303,6 +309,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting WebAuthnService, UserBlockingService, CacheService, + UserService, UserFollowingService, UserKeypairService, UserListService, @@ -315,6 +322,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FileInfoService, SearchService, ClipService, + FeaturedService, + RedisTimelineService, ChartLoggerService, FederationChart, NotesChart, @@ -426,6 +435,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $WebAuthnService, $UserBlockingService, $CacheService, + $UserService, $UserFollowingService, $UserKeypairService, $UserListService, @@ -438,6 +448,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FileInfoService, $SearchService, $ClipService, + $FeaturedService, + $RedisTimelineService, $ChartLoggerService, $FederationChart, $NotesChart, @@ -550,6 +562,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting WebAuthnService, UserBlockingService, CacheService, + UserService, UserFollowingService, UserKeypairService, UserListService, @@ -562,6 +575,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FileInfoService, SearchService, ClipService, + FeaturedService, + RedisTimelineService, FederationChart, NotesChart, UsersChart, @@ -672,6 +687,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $WebAuthnService, $UserBlockingService, $CacheService, + $UserService, $UserFollowingService, $UserKeypairService, $UserListService, @@ -684,6 +700,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FileInfoService, $SearchService, $ClipService, + $FeaturedService, + $RedisTimelineService, $FederationChart, $NotesChart, $UsersChart, diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 9661a0aea3327c902c0847daf77143cc95f83a90..145c224f678af655094454d62a3c4485b1792b08 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -48,7 +48,6 @@ export class CustomEmojiService implements OnApplicationShutdown { fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))), toRedisConverter: (value) => JSON.stringify(Array.from(value.values())), fromRedisConverter: (value) => { - if (!Array.isArray(JSON.parse(value))) return undefined; // å¤ã„ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®å£Šã‚ŒãŸã‚ャッシュãŒæ®‹ã£ã¦ã„ã‚‹ã“ã¨ãŒã‚ã‚‹(ãã®ã†ã¡æ¶ˆã™) return new Map(JSON.parse(value).map((x: Serialized<MiEmoji>) => [x.name, { ...x, updatedAt: x.updatedAt ? new Date(x.updatedAt) : null, @@ -380,6 +379,20 @@ export class CustomEmojiService implements OnApplicationShutdown { } } + /** + * ãƒãƒ¼ã‚«ãƒ«å†…ã®çµµæ–‡å—ã«é‡è¤‡ãŒãªã„ã‹ãƒã‚§ãƒƒã‚¯ã—ã¾ã™ + * @param name 絵文å—å + */ + @bindThis + public checkDuplicate(name: string): Promise<boolean> { + return this.emojisRepository.exist({ where: { name, host: IsNull() } }); + } + + @bindThis + public getEmojiById(id: string): Promise<MiEmoji | null> { + return this.emojisRepository.findOneBy({ id }); + } + @bindThis public dispose(): void { this.cache.dispose(); diff --git a/packages/backend/src/core/FeaturedService.ts b/packages/backend/src/core/FeaturedService.ts new file mode 100644 index 0000000000000000000000000000000000000000..cccbbd95cb86fce97f7781d6524ff9cf3e25a366 --- /dev/null +++ b/packages/backend/src/core/FeaturedService.ts @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; +import type { MiNote, MiUser } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; + +const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3æ—¥ã”㨠+const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ã”㨠+const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ã”㨠+ +@Injectable() +export class FeaturedService { + constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, // TODO: 専用ã®Redisサーãƒãƒ¼ã‚’è¨å®šã§ãるよã†ã«ã™ã‚‹ + ) { + } + + @bindThis + private getCurrentWindow(windowRange: number): number { + const passed = new Date().getTime() - new Date(new Date().getFullYear(), 0, 1).getTime(); + return Math.floor(passed / windowRange); + } + + @bindThis + private async updateRankingOf(name: string, windowRange: number, element: string, score = 1): Promise<void> { + const currentWindow = this.getCurrentWindow(windowRange); + const redisTransaction = this.redisClient.multi(); + redisTransaction.zincrby( + `${name}:${currentWindow}`, + score, + element); + redisTransaction.expire( + `${name}:${currentWindow}`, + (windowRange * 3) / 1000, + 'NX'); // "NX -- Set expiry only when the key has no expiry" = 有効期é™ãŒãªã„ã¨ãã ã‘è¨å®š + await redisTransaction.exec(); + } + + @bindThis + private async getRankingOf(name: string, windowRange: number, threshold: number): Promise<string[]> { + const currentWindow = this.getCurrentWindow(windowRange); + const previousWindow = currentWindow - 1; + + const redisPipeline = this.redisClient.pipeline(); + redisPipeline.zrange( + `${name}:${currentWindow}`, 0, threshold, 'REV', 'WITHSCORES'); + redisPipeline.zrange( + `${name}:${previousWindow}`, 0, threshold, 'REV', 'WITHSCORES'); + const [currentRankingResult, previousRankingResult] = await redisPipeline.exec().then(result => result ? result.map(r => r[1] as string[]) : [[], []]); + + const ranking = new Map<string, number>(); + for (let i = 0; i < currentRankingResult.length; i += 2) { + const noteId = currentRankingResult[i]; + const score = parseInt(currentRankingResult[i + 1], 10); + ranking.set(noteId, score); + } + for (let i = 0; i < previousRankingResult.length; i += 2) { + const noteId = previousRankingResult[i]; + const score = parseInt(previousRankingResult[i + 1], 10); + const exist = ranking.get(noteId); + if (exist != null) { + ranking.set(noteId, (exist + score) / 2); + } else { + ranking.set(noteId, score); + } + } + + return Array.from(ranking.keys()); + } + + @bindThis + public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> { + return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score); + } + + @bindThis + public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise<void> { + return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score); + } + + @bindThis + public updatePerUserNotesRanking(userId: MiUser['id'], noteId: MiNote['id'], score = 1): Promise<void> { + return this.updateRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, noteId, score); + } + + @bindThis + public updateHashtagsRanking(hashtag: string, score = 1): Promise<void> { + return this.updateRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, hashtag, score); + } + + @bindThis + public getGlobalNotesRanking(threshold: number): Promise<MiNote['id'][]> { + return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, threshold); + } + + @bindThis + public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise<MiNote['id'][]> { + return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold); + } + + @bindThis + public getPerUserNotesRanking(userId: MiUser['id'], threshold: number): Promise<MiNote['id'][]> { + return this.getRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, threshold); + } + + @bindThis + public getHashtagsRanking(threshold: number): Promise<string[]> { + return this.getRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, threshold); + } +} diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index c72c7460ff21bb00264e77cb4c37de49e6b7516a..ddff28359a08db6db5797d5f69f7e90f5ec05dbf 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -4,6 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import type { MiUser } from '@/models/User.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; @@ -12,15 +13,22 @@ import type { MiHashtag } from '@/models/Hashtag.js'; import type { HashtagsRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; +import { MetaService } from '@/core/MetaService.js'; @Injectable() export class HashtagService { constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, // TODO: 専用ã®Redisサーãƒãƒ¼ã‚’è¨å®šã§ãるよã†ã«ã™ã‚‹ + @Inject(DI.hashtagsRepository) private hashtagsRepository: HashtagsRepository, private userEntityService: UserEntityService, + private featuredService: FeaturedService, private idService: IdService, + private metaService: MetaService, ) { } @@ -46,6 +54,9 @@ export class HashtagService { public async updateHashtag(user: { id: MiUser['id']; host: MiUser['host']; }, tag: string, isUserAttached = false, inc = true) { tag = normalizeForSearch(tag); + // TODO: サンプリング + this.updateHashtagsRanking(tag, user.id); + const index = await this.hashtagsRepository.findOneBy({ name: tag }); if (index == null && !inc) return; @@ -85,7 +96,7 @@ export class HashtagService { } } } else { - // 自分ãŒåˆã‚ã¦ã“ã®ã‚¿ã‚°ã‚’使ã£ãŸãªã‚‰ + // 自分ãŒåˆã‚ã¦ã“ã®ã‚¿ã‚°ã‚’使ã£ãŸãªã‚‰ if (!index.mentionedUserIds.some(id => id === user.id)) { set.mentionedUserIds = () => `array_append("mentionedUserIds", '${user.id}')`; set.mentionedUsersCount = () => '"mentionedUsersCount" + 1'; @@ -144,4 +155,94 @@ export class HashtagService { } } } + + @bindThis + public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise<void> { + const instance = await this.metaService.fetch(); + const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); + if (hiddenTags.includes(hashtag)) return; + + // YYYYMMDDHHmm (10分間隔) + const now = new Date(); + now.setMinutes(Math.floor(now.getMinutes() / 10) * 10, 0, 0); + const window = `${now.getUTCFullYear()}${(now.getUTCMonth() + 1).toString().padStart(2, '0')}${now.getUTCDate().toString().padStart(2, '0')}${now.getUTCHours().toString().padStart(2, '0')}${now.getUTCMinutes().toString().padStart(2, '0')}`; + + const exist = await this.redisClient.sismember(`hashtagUsers:${hashtag}`, userId); + if (exist === 1) return; + + this.featuredService.updateHashtagsRanking(hashtag, 1); + + const redisPipeline = this.redisClient.pipeline(); + + // ãƒãƒ£ãƒ¼ãƒˆç”¨ + redisPipeline.pfadd(`hashtagUsers:${hashtag}:${window}`, userId); + redisPipeline.expire(`hashtagUsers:${hashtag}:${window}`, + 60 * 60 * 24 * 3, // 3日間 + 'NX', // "NX -- Set expiry only when the key has no expiry" = 有効期é™ãŒãªã„ã¨ãã ã‘è¨å®š + ); + + // ユニークカウント用 + // TODO: Bloom Filter を使ã†ã‚ˆã†ã«ã—ã¦ã‚‚良ã•ãㆠ+ redisPipeline.sadd(`hashtagUsers:${hashtag}`, userId); + redisPipeline.expire(`hashtagUsers:${hashtag}`, + 60 * 60, // 1時間 + 'NX', // "NX -- Set expiry only when the key has no expiry" = 有効期é™ãŒãªã„ã¨ãã ã‘è¨å®š + ); + + redisPipeline.exec(); + } + + @bindThis + public async getChart(hashtag: string, range: number): Promise<number[]> { + const now = new Date(); + now.setMinutes(Math.floor(now.getMinutes() / 10) * 10, 0, 0); + + const redisPipeline = this.redisClient.pipeline(); + + for (let i = 0; i < range; i++) { + const window = `${now.getUTCFullYear()}${(now.getUTCMonth() + 1).toString().padStart(2, '0')}${now.getUTCDate().toString().padStart(2, '0')}${now.getUTCHours().toString().padStart(2, '0')}${now.getUTCMinutes().toString().padStart(2, '0')}`; + redisPipeline.pfcount(`hashtagUsers:${hashtag}:${window}`); + now.setMinutes(now.getMinutes() - (i * 10), 0, 0); + } + + const result = await redisPipeline.exec(); + + if (result == null) return []; + + return result.map(x => x[1]) as number[]; + } + + @bindThis + public async getCharts(hashtags: string[], range: number): Promise<Record<string, number[]>> { + const now = new Date(); + now.setMinutes(Math.floor(now.getMinutes() / 10) * 10, 0, 0); + + const redisPipeline = this.redisClient.pipeline(); + + for (let i = 0; i < range; i++) { + const window = `${now.getUTCFullYear()}${(now.getUTCMonth() + 1).toString().padStart(2, '0')}${now.getUTCDate().toString().padStart(2, '0')}${now.getUTCHours().toString().padStart(2, '0')}${now.getUTCMinutes().toString().padStart(2, '0')}`; + for (const hashtag of hashtags) { + redisPipeline.pfcount(`hashtagUsers:${hashtag}:${window}`); + } + now.setMinutes(now.getMinutes() - (i * 10), 0, 0); + } + + const result = await redisPipeline.exec(); + + if (result == null) return {}; + + // key is hashtag + const charts = {} as Record<string, number[]>; + for (const hashtag of hashtags) { + charts[hashtag] = []; + } + + for (let i = 0; i < range; i++) { + for (let j = 0; j < hashtags.length; j++) { + charts[hashtags[j]].push(result[(i * hashtags.length) + j][1] as number); + } + } + + return charts; + } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index f20727ce418c90dbc5c14b2a90ba11cf6c5273a1..2a734671228a278501c28075b31c3f97b3e4c5b2 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -5,7 +5,7 @@ import { setImmediate } from 'node:timers/promises'; import * as mfm from 'mfm-js'; -import { In, DataSource } from 'typeorm'; +import { In, DataSource, IsNull, LessThan } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import RE2 from 're2'; @@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; -import type { ChannelsRepository, FollowingsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; @@ -53,8 +53,8 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; - -const mutedWordsCache = new MemorySingleCache<{ userId: MiUserProfile['userId']; mutedWords: MiUserProfile['mutedWords']; }[]>(1000 * 60 * 5); +import { FeaturedService } from '@/core/FeaturedService.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -157,8 +157,8 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.db) private db: DataSource, - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -175,8 +175,8 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - @Inject(DI.mutedNotesRepository) - private mutedNotesRepository: MutedNotesRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, @@ -187,11 +187,15 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private idService: IdService, private globalEventService: GlobalEventService, private queueService: QueueService, + private redisTimelineService: RedisTimelineService, private noteReadService: NoteReadService, private notificationService: NotificationService, private relayService: RelayService, @@ -199,6 +203,7 @@ export class NoteCreateService implements OnApplicationShutdown { private hashtagService: HashtagService, private antennaService: AntennaService, private webhookService: WebhookService, + private featuredService: FeaturedService, private remoteUserResolveService: RemoteUserResolveService, private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, @@ -251,19 +256,30 @@ export class NoteCreateService implements OnApplicationShutdown { } } - // Renote対象ãŒã€Œãƒ›ãƒ¼ãƒ ã¾ãŸã¯å…¨ä½“ã€ä»¥å¤–ã®å…¬é–‹ç¯„囲ãªã‚‰reject - if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) { - throw new Error('Renote target is not public or home'); - } - - // Renote対象ãŒpublicã§ã¯ãªã„ãªã‚‰homeã«ã™ã‚‹ - if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') { - data.visibility = 'home'; - } + if (data.renote) { + switch (data.renote.visibility) { + case 'public': + // public noteã¯ç„¡æ¡ä»¶ã«renoteå¯èƒ½ + break; + case 'home': + // home noteã¯home以下ã«renoteå¯èƒ½ + if (data.visibility === 'public') { + data.visibility = 'home'; + } + break; + case 'followers': + // 他人ã®followers noteã¯reject + if (data.renote.userId !== user.id) { + throw new Error('Renote target is not public or home'); + } - // Renote対象ãŒfollowersãªã‚‰followersã«ã™ã‚‹ - if (data.renote && data.renote.visibility === 'followers') { - data.visibility = 'followers'; + // Renote対象ãŒfollowersãªã‚‰followersã«ã™ã‚‹ + data.visibility = 'followers'; + break; + case 'specified': + // specified / direct noteã¯reject + throw new Error('Renote target is not public or home'); + } } // 返信対象ãŒpublicã§ã¯ãªã„ãªã‚‰homeã«ã™ã‚‹ @@ -333,14 +349,6 @@ export class NoteCreateService implements OnApplicationShutdown { const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); - if (data.channel) { - this.redisClient.xadd( - `channelTimeline:${data.channel.id}`, - 'MAXLEN', '~', this.config.perChannelMaxNoteCacheCount.toString(), - '*', - 'note', note.id); - } - setImmediate('post created', { signal: this.#shutdownController.signal }).then( () => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!), () => { /* aborted, ignore this */ }, @@ -480,26 +488,7 @@ export class NoteCreateService implements OnApplicationShutdown { // Increment notes count (user) this.incNotesCountOfUser(user); - // Word mute - mutedWordsCache.fetch(() => this.userProfilesRepository.find({ - where: { - enableWordMute: true, - }, - select: ['userId', 'mutedWords'], - })).then(us => { - for (const u of us) { - checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => { - if (shouldMute) { - this.mutedNotesRepository.insert({ - id: this.idService.genId(), - userId: u.userId, - noteId: note.id, - reason: 'word', - }); - } - }); - } - }); + this.pushToTl(note, user); this.antennaService.addNoteToAntennas(note, user); @@ -508,11 +497,13 @@ export class NoteCreateService implements OnApplicationShutdown { } if (data.reply == null) { + // TODO: ã‚ャッシュ this.followingsRepository.findBy({ followeeId: user.id, notify: 'normal', }).then(followings => { for (const following of followings) { + // TODO: ワードミュート考慮 this.notificationService.createNotification(following.followerId, 'note', { noteId: note.id, }, user.id); @@ -520,9 +511,8 @@ export class NoteCreateService implements OnApplicationShutdown { }); } - // ã“ã®æŠ•ç¨¿ã‚’除ã指定ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ã‚ˆã‚‹æŒ‡å®šã—ãŸãƒŽãƒ¼ãƒˆã®ãƒªãƒŽãƒ¼ãƒˆãŒå˜åœ¨ã—ãªã„ã¨ã - if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) { - if (!user.isBot) this.incRenoteCount(data.renote); + if (data.renote && data.renote.userId !== user.id && !user.isBot) { + this.incRenoteCount(data.renote); } if (data.poll && data.poll.expiresAt) { @@ -722,10 +712,23 @@ export class NoteCreateService implements OnApplicationShutdown { this.notesRepository.createQueryBuilder().update() .set({ renoteCount: () => '"renoteCount" + 1', - score: () => '"score" + 1', }) .where('id = :id', { id: renote.id }) .execute(); + + // 30%ã®ç¢ºçŽ‡ã€3日以内ã«æŠ•ç¨¿ã•ã‚ŒãŸãƒŽãƒ¼ãƒˆã®å ´åˆãƒã‚¤ãƒ©ã‚¤ãƒˆç”¨ãƒ©ãƒ³ã‚ング更新 + if (Math.random() < 0.3 && (Date.now() - this.idService.parse(renote.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3) { + if (renote.channelId != null) { + if (renote.replyId == null) { + this.featuredService.updateInChannelNotesRanking(renote.channelId, renote.id, 5); + } + } else { + if (renote.visibility === 'public' && renote.userHost == null && renote.replyId == null) { + this.featuredService.updateGlobalNotesRanking(renote.id, 5); + this.featuredService.updatePerUserNotesRanking(renote.userId, renote.id, 5); + } + } + } } @bindThis @@ -811,6 +814,161 @@ export class NoteCreateService implements OnApplicationShutdown { return mentionedUsers; } + @bindThis + private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) { + const meta = await this.metaService.fetch(); + + const r = this.redisForTimelines.pipeline(); + + if (note.channelId) { + this.redisTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); + + this.redisTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + + const channelFollowings = await this.channelFollowingsRepository.find({ + where: { + followeeId: note.channelId, + }, + select: ['followerId'], + }); + + for (const channelFollowing of channelFollowings) { + this.redisTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + if (note.fileIds.length > 0) { + this.redisTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + } + } + } else { + // TODO: ã‚ャッシュ? + // eslint-disable-next-line prefer-const + let [followings, userListMemberships] = await Promise.all([ + this.followingsRepository.find({ + where: { + followeeId: user.id, + followerHost: IsNull(), + isFollowerHibernated: false, + }, + select: ['followerId', 'withReplies'], + }), + this.userListMembershipsRepository.find({ + where: { + userId: user.id, + }, + select: ['userListId', 'userListUserId', 'withReplies'], + }), + ]); + + if (note.visibility === 'followers') { + // TODO: é‡ãã†ã ã‹ã‚‰ä½•ã¨ã‹ã—ãŸã„ Set 使ã†ï¼Ÿ + userListMemberships = userListMemberships.filter(x => followings.some(f => f.followerId === x.userListUserId)); + } + + // TODO: ã‚ã¾ã‚Šã«ã‚‚æ•°ãŒå¤šã„㨠redisPipeline.exec ã«å¤±æ•—ã™ã‚‹(ç†ç”±ã¯ä¸æ˜Ž)ãŸã‚ã€3万件程度を目安ã«åˆ†å‰²ã—ã¦å®Ÿè¡Œã™ã‚‹ã‚ˆã†ã«ã™ã‚‹ + for (const following of followings) { + // 基本的ã«visibleUserIdsã«ã¯è‡ªèº«ã®idãŒå«ã¾ã‚Œã¦ã„ã‚‹å‰æã§ã‚ã‚‹ã“㨠+ if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue; + + // 自分自身以外ã¸ã®è¿”ä¿¡ + if (note.replyId && note.replyUserId !== note.userId) { + if (!following.withReplies) continue; + } + + this.redisTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + if (note.fileIds.length > 0) { + this.redisTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + } + } + + for (const userListMembership of userListMemberships) { + // ダイレクトã®ã¨ãã€ãã®ãƒªã‚¹ãƒˆãŒå¯¾è±¡å¤–ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®å ´åˆ + if ( + note.visibility === 'specified' && + !note.visibleUserIds.some(v => v === userListMembership.userListUserId) + ) continue; + + // 自分自身以外ã¸ã®è¿”ä¿¡ + if (note.replyId && note.replyUserId !== note.userId) { + if (!userListMembership.withReplies) continue; + } + + this.redisTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); + if (note.fileIds.length > 0) { + this.redisTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); + } + } + + if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身ã®HTL + this.redisTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); + if (note.fileIds.length > 0) { + this.redisTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + } + } + + // 自分自身以外ã¸ã®è¿”ä¿¡ + if (note.replyId && note.replyUserId !== note.userId) { + this.redisTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + } else { + this.redisTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + if (note.fileIds.length > 0) { + this.redisTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); + } + + if (note.visibility === 'public' && note.userHost == null) { + this.redisTimelineService.push('localTimeline', note.id, 1000, r); + if (note.fileIds.length > 0) { + this.redisTimelineService.push('localTimelineWithFiles', note.id, 500, r); + } + } + } + + if (Math.random() < 0.1) { + process.nextTick(() => { + this.checkHibernation(followings); + }); + } + } + + r.exec(); + } + + @bindThis + public async checkHibernation(followings: MiFollowing[]) { + if (followings.length === 0) return; + + const shuffle = (array: MiFollowing[]) => { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + }; + + // ランダムã«æœ€å¤§1000件サンプリング + const samples = shuffle(followings).slice(0, Math.min(followings.length, 1000)); + + const hibernatedUsers = await this.usersRepository.find({ + where: { + id: In(samples.map(x => x.followerId)), + lastActiveDate: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 50))), + }, + select: ['id'], + }); + + if (hibernatedUsers.length > 0) { + this.usersRepository.update({ + id: In(hibernatedUsers.map(x => x.id)), + }, { + isHibernated: true, + }); + + this.followingsRepository.update({ + followerId: In(hibernatedUsers.map(x => x.id)), + }, { + isFollowerHibernated: true, + }); + } + } + @bindThis public dispose(): void { this.#shutdownController.abort(); diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 87979f22ac581e54c7dba357e649868b04962b72..9a817ffd76ccccc07362de47feb5ef110f869786 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -64,12 +64,6 @@ export class NoteDeleteService { const deletedAt = new Date(); const cascadingNotes = await this.findCascadingNotes(note); - // ã“ã®æŠ•ç¨¿ã‚’除ã指定ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ã‚ˆã‚‹æŒ‡å®šã—ãŸãƒŽãƒ¼ãƒˆã®ãƒªãƒŽãƒ¼ãƒˆãŒå˜åœ¨ã—ãªã„ã¨ã - if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) { - this.notesRepository.decrement({ id: note.renoteId }, 'renoteCount', 1); - if (!user.isBot) this.notesRepository.decrement({ id: note.renoteId }, 'score', 1); - } - if (note.replyId) { await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1); } diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index ca05989a4a67749a2ca428d3136f32f3695722c1..32d54d257688865235a236da81d436d0ad7383ab 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -99,19 +99,19 @@ export class NotificationService implements OnApplicationShutdown { } if (recieveConfig?.type === 'following') { - const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId)); + const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)); if (!isFollowing) { return null; } } else if (recieveConfig?.type === 'follower') { - const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId)); + const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)); if (!isFollower) { return null; } } else if (recieveConfig?.type === 'mutualFollow') { const [isFollowing, isFollower] = await Promise.all([ - this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId)), - this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId)), + this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)), + this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)), ]); if (!isFollowing && !isFollower) { return null; diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index 940aa98347eb885f14afece677bb9604d250904e..570f2350f11a66d3c7e1117fdeabc357f4893494 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -96,6 +96,8 @@ export class PollService { const note = await this.notesRepository.findOneBy({ id: noteId }); if (note == null) throw new Error('note not found'); + if (note.localOnly) return; + const user = await this.usersRepository.findOneBy({ id: note.userId }); if (user == null) throw new Error('note not found'); diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index 9145726f86f9f720f4f213d1314b8033eaf23cda..50d1d2e65b97217530d7836e45ce7c9fb46ecd88 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -7,8 +7,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets, ObjectLiteral } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { MiUser } from '@/models/User.js'; -import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js'; +import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; +import { IdService } from '@/core/IdService.js'; import type { SelectQueryBuilder } from 'typeorm'; @Injectable() @@ -23,9 +24,6 @@ export class QueryService { @Inject(DI.channelFollowingsRepository) private channelFollowingsRepository: ChannelFollowingsRepository, - @Inject(DI.mutedNotesRepository) - private mutedNotesRepository: MutedNotesRepository, - @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -37,6 +35,8 @@ export class QueryService { @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, + + private idService: IdService, ) { } @@ -52,15 +52,15 @@ export class QueryService { q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); q.orderBy(`${q.alias}.id`, 'DESC'); } else if (sinceDate && untilDate) { - q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); - q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); - q.orderBy(`${q.alias}.createdAt`, 'DESC'); + q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.genId(new Date(sinceDate)) }); + q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.genId(new Date(untilDate)) }); + q.orderBy(`${q.alias}.id`, 'DESC'); } else if (sinceDate) { - q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); - q.orderBy(`${q.alias}.createdAt`, 'ASC'); + q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.genId(new Date(sinceDate)) }); + q.orderBy(`${q.alias}.id`, 'ASC'); } else if (untilDate) { - q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); - q.orderBy(`${q.alias}.createdAt`, 'DESC'); + q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.genId(new Date(untilDate)) }); + q.orderBy(`${q.alias}.id`, 'DESC'); } else { q.orderBy(`${q.alias}.id`, 'DESC'); } @@ -79,13 +79,15 @@ export class QueryService { // 投稿ã®å¼•ç”¨å…ƒã®ä½œè€…ã«ãƒ–ãƒãƒƒã‚¯ã•ã‚Œã¦ã„ãªã„ q .andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`) - .andWhere(new Brackets(qb => { qb - .where('note.replyUserId IS NULL') - .orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`); + .andWhere(new Brackets(qb => { + qb + .where('note.replyUserId IS NULL') + .orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`); })) - .andWhere(new Brackets(qb => { qb - .where('note.renoteUserId IS NULL') - .orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`); + .andWhere(new Brackets(qb => { + qb + .where('note.renoteUserId IS NULL') + .orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`); })); q.setParameters(blockingQuery.getParameters()); @@ -108,39 +110,6 @@ export class QueryService { q.setParameters(blockedQuery.getParameters()); } - @bindThis - public generateChannelQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void { - if (me == null) { - q.andWhere('note.channelId IS NULL'); - } else { - q.leftJoinAndSelect('note.channel', 'channel'); - - const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing') - .select('channelFollowing.followeeId') - .where('channelFollowing.followerId = :followerId', { followerId: me.id }); - - q.andWhere(new Brackets(qb => { qb - // ãƒãƒ£ãƒ³ãƒãƒ«ã®ãƒŽãƒ¼ãƒˆã§ã¯ãªã„ - .where('note.channelId IS NULL') - // ã¾ãŸã¯è‡ªåˆ†ãŒãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„ã‚‹ãƒãƒ£ãƒ³ãƒãƒ«ã®ãƒŽãƒ¼ãƒˆ - .orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`); - })); - - q.setParameters(channelFollowingQuery.getParameters()); - } - } - - @bindThis - public generateMutedNoteQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void { - const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted') - .select('muted.noteId') - .where('muted.userId = :userId', { userId: me.id }); - - q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); - - q.setParameters(mutedQuery.getParameters()); - } - @bindThis public generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void { const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted') @@ -148,16 +117,17 @@ export class QueryService { .where('threadMuted.userId = :userId', { userId: me.id }); q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); - q.andWhere(new Brackets(qb => { qb - .where('note.threadId IS NULL') - .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`); + q.andWhere(new Brackets(qb => { + qb + .where('note.threadId IS NULL') + .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`); })); q.setParameters(mutedQuery.getParameters()); } @bindThis - public generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: MiUser): void { + public generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void { const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') .select('muting.muteeId') .where('muting.muterId = :muterId', { muterId: me.id }); @@ -175,26 +145,31 @@ export class QueryService { // 投稿ã®å¼•ç”¨å…ƒã®ä½œè€…をミュートã—ã¦ã„ãªã„ q .andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`) - .andWhere(new Brackets(qb => { qb - .where('note.replyUserId IS NULL') - .orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`); + .andWhere(new Brackets(qb => { + qb + .where('note.replyUserId IS NULL') + .orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`); })) - .andWhere(new Brackets(qb => { qb - .where('note.renoteUserId IS NULL') - .orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`); + .andWhere(new Brackets(qb => { + qb + .where('note.renoteUserId IS NULL') + .orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`); })) // mute instances - .andWhere(new Brackets(qb => { qb - .andWhere('note.userHost IS NULL') - .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`); + .andWhere(new Brackets(qb => { + qb + .andWhere('note.userHost IS NULL') + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`); })) - .andWhere(new Brackets(qb => { qb - .where('note.replyUserHost IS NULL') - .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`); + .andWhere(new Brackets(qb => { + qb + .where('note.replyUserHost IS NULL') + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`); })) - .andWhere(new Brackets(qb => { qb - .where('note.renoteUserHost IS NULL') - .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`); + .andWhere(new Brackets(qb => { + qb + .where('note.renoteUserHost IS NULL') + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`); })); q.setParameters(mutingQuery.getParameters()); @@ -212,66 +187,45 @@ export class QueryService { q.setParameters(mutingQuery.getParameters()); } - @bindThis - public generateRepliesQuery(q: SelectQueryBuilder<any>, withReplies: boolean, me?: Pick<MiUser, 'id'> | null): void { - if (me == null) { - q.andWhere(new Brackets(qb => { qb - .where('note.replyId IS NULL') // 返信ã§ã¯ãªã„ - .orWhere(new Brackets(qb => { qb // 返信ã ã‘ã©æŠ•ç¨¿è€…自身ã¸ã®è¿”ä¿¡ - .where('note.replyId IS NOT NULL') - .andWhere('note.replyUserId = note.userId'); - })); - })); - } else if (!withReplies) { - q.andWhere(new Brackets(qb => { qb - .where('note.replyId IS NULL') // 返信ã§ã¯ãªã„ - .orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信ã ã‘ã©è‡ªåˆ†ã®ãƒŽãƒ¼ãƒˆã¸ã®è¿”ä¿¡ - .orWhere(new Brackets(qb => { qb // 返信ã ã‘ã©è‡ªåˆ†ã®è¡Œã£ãŸè¿”ä¿¡ - .where('note.replyId IS NOT NULL') - .andWhere('note.userId = :meId', { meId: me.id }); - })) - .orWhere(new Brackets(qb => { qb // 返信ã ã‘ã©æŠ•ç¨¿è€…自身ã¸ã®è¿”ä¿¡ - .where('note.replyId IS NOT NULL') - .andWhere('note.replyUserId = note.userId'); - })); - })); - } - } - @bindThis public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void { // This code must always be synchronized with the checks in Notes.isVisibleForMe. if (me == null) { - q.andWhere(new Brackets(qb => { qb - .where('note.visibility = \'public\'') - .orWhere('note.visibility = \'home\''); + q.andWhere(new Brackets(qb => { + qb + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); })); } else { const followingQuery = this.followingsRepository.createQueryBuilder('following') .select('following.followeeId') .where('following.followerId = :meId'); - q.andWhere(new Brackets(qb => { qb + q.andWhere(new Brackets(qb => { + qb // 公開投稿ã§ã‚ã‚‹ - .where(new Brackets(qb => { qb - .where('note.visibility = \'public\'') - .orWhere('note.visibility = \'home\''); - })) + .where(new Brackets(qb => { + qb + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); + })) // ã¾ãŸã¯ 自分自身 - .orWhere('note.userId = :meId') + .orWhere('note.userId = :meId') // ã¾ãŸã¯ 自分宛㦠- .orWhere(':meId = ANY(note.visibleUserIds)') - .orWhere(':meId = ANY(note.mentions)') - .orWhere(new Brackets(qb => { qb - // ã¾ãŸã¯ フォãƒãƒ¯ãƒ¼å®›ã¦ã®æŠ•ç¨¿ã§ã‚り〠- .where('note.visibility = \'followers\'') - .andWhere(new Brackets(qb => { qb - // 自分ãŒãƒ•ã‚©ãƒãƒ¯ãƒ¼ã§ã‚ã‚‹ - .where(`note.userId IN (${ followingQuery.getQuery() })`) - // ã¾ãŸã¯ 自分ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライ - .orWhere('note.replyUserId = :meId'); + .orWhere(':meId = ANY(note.visibleUserIds)') + .orWhere(':meId = ANY(note.mentions)') + .orWhere(new Brackets(qb => { + qb + // ã¾ãŸã¯ フォãƒãƒ¯ãƒ¼å®›ã¦ã®æŠ•ç¨¿ã§ã‚り〠+ .where('note.visibility = \'followers\'') + .andWhere(new Brackets(qb => { + qb + // 自分ãŒãƒ•ã‚©ãƒãƒ¯ãƒ¼ã§ã‚ã‚‹ + .where(`note.userId IN (${ followingQuery.getQuery() })`) + // ã¾ãŸã¯ 自分ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライ + .orWhere('note.replyUserId = :meId'); + })); })); - })); })); q.setParameters({ meId: me.id }); diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 25464b19a8aa8c5642ec6238c26f1bcbc372a510..e7bbd449262738ac02b3d0f1d6035054a20221e2 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -4,6 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; @@ -26,6 +27,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { RoleService } from '@/core/RoleService.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; const FALLBACK = 'â¤'; @@ -66,6 +68,9 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/; @Injectable() export class ReactionService { constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -86,6 +91,7 @@ export class ReactionService { private noteEntityService: NoteEntityService, private userBlockingService: UserBlockingService, private idService: IdService, + private featuredService: FeaturedService, private globalEventService: GlobalEventService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, @@ -182,11 +188,28 @@ export class ReactionService { await this.notesRepository.createQueryBuilder().update() .set({ reactions: () => sql, - ... (!user.isBot ? { score: () => '"score" + 1' } : {}), }) .where('id = :id', { id: note.id }) .execute(); + // 30%ã®ç¢ºçŽ‡ã€ã‚»ãƒ«ãƒ•ã§ã¯ãªã„ã€3日以内ã«æŠ•ç¨¿ã•ã‚ŒãŸãƒŽãƒ¼ãƒˆã®å ´åˆãƒã‚¤ãƒ©ã‚¤ãƒˆç”¨ãƒ©ãƒ³ã‚ング更新 + if ( + Math.random() < 0.3 && + note.userId !== user.id && + (Date.now() - this.idService.parse(note.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3 + ) { + if (note.channelId != null) { + if (note.replyId == null) { + this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1); + } + } else { + if (note.visibility === 'public' && note.userHost == null && note.replyId == null) { + this.featuredService.updateGlobalNotesRanking(note.id, 1); + this.featuredService.updatePerUserNotesRanking(note.userId, note.id, 1); + } + } + } + const meta = await this.metaService.fetch(); if (meta.enableChartsForRemoteUser || (user.host == null)) { @@ -275,8 +298,6 @@ export class ReactionService { .where('id = :id', { id: note.id }) .execute(); - if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1); - this.globalEventService.publishNoteStream(note.id, 'unreacted', { reaction: this.decodeReaction(exist.reaction).reaction, userId: user.id, diff --git a/packages/backend/src/core/RedisTimelineService.ts b/packages/backend/src/core/RedisTimelineService.ts new file mode 100644 index 0000000000000000000000000000000000000000..94541759cc08d07f8852654e486c56148462118e --- /dev/null +++ b/packages/backend/src/core/RedisTimelineService.ts @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { IdService } from '@/core/IdService.js'; + +@Injectable() +export class RedisTimelineService { + constructor( + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + + private idService: IdService, + ) { + } + + @bindThis + public push(tl: string, id: string, maxlen: number, pipeline: Redis.ChainableCommander) { + // リモートã‹ã‚‰é…ã‚Œã¦å±Šã„ãŸ(ã‚‚ã—ãã¯å¾Œã‹ã‚‰è¿½åŠ ã•ã‚ŒãŸ)投稿日時ãŒå¤ã„投稿ãŒè¿½åŠ ã•ã‚Œã‚‹ã¨ãƒšãƒ¼ã‚¸ãƒãƒ¼ã‚·ãƒ§ãƒ³æ™‚ã«å•é¡Œã‚’引ãèµ·ã“ã™ãŸã‚〠+ // 3分以内ã«æŠ•ç¨¿ã•ã‚ŒãŸã‚‚ã®ã§ãªã„å ´åˆã€Redisã«ã‚る最å¤ã®IDより新ã—ã„å ´åˆã®ã¿è¿½åŠ ã™ã‚‹ + if (this.idService.parse(id).date.getTime() > Date.now() - 1000 * 60 * 3) { + pipeline.lpush('list:' + tl, id); + if (Math.random() < 0.1) { // 10%ã®ç¢ºçŽ‡ã§ãƒˆãƒªãƒ + pipeline.ltrim('list:' + tl, 0, maxlen - 1); + } + } else { + // 末尾ã®IDã‚’å–å¾— + this.redisForTimelines.lindex('list:' + tl, -1).then(lastId => { + if (lastId == null || (this.idService.parse(id).date.getTime() > this.idService.parse(lastId).date.getTime())) { + this.redisForTimelines.lpush('list:' + tl, id); + } else { + Promise.resolve(); + } + }); + } + } + + @bindThis + public get(name: string, untilId?: string | null, sinceId?: string | null) { + if (untilId && sinceId) { + return this.redisForTimelines.lrange('list:' + name, 0, -1) + .then(ids => ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1)); + } else if (untilId) { + return this.redisForTimelines.lrange('list:' + name, 0, -1) + .then(ids => ids.filter(id => id < untilId).sort((a, b) => a > b ? -1 : 1)); + } else if (sinceId) { + return this.redisForTimelines.lrange('list:' + name, 0, -1) + .then(ids => ids.filter(id => id > sinceId).sort((a, b) => a < b ? -1 : 1)); + } else { + return this.redisForTimelines.lrange('list:' + name, 0, -1) + .then(ids => ids.sort((a, b) => a > b ? -1 : 1)); + } + } + + @bindThis + public getMulti(name: string[], untilId?: string | null, sinceId?: string | null): Promise<string[][]> { + const pipeline = this.redisForTimelines.pipeline(); + for (const n of name) { + pipeline.lrange('list:' + n, 0, -1); + } + return pipeline.exec().then(res => { + if (res == null) return []; + const tls = res.map(r => r[1] as string[]); + return tls.map(ids => + (untilId && sinceId) + ? ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1) + : untilId + ? ids.filter(id => id < untilId).sort((a, b) => a > b ? -1 : 1) + : sinceId + ? ids.filter(id => id > sinceId).sort((a, b) => a < b ? -1 : 1) + : ids.sort((a, b) => a > b ? -1 : 1), + ); + }); + } +} diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index ad40fbaecd4ad1b067203d305afcfb89c166fabd..2c3547e4acd9d864d4dbabbc918f6dd7538127cf 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -20,13 +20,13 @@ import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import type { Packed } from '@/misc/json-schema.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; export type RolePolicies = { gtlAvailable: boolean; ltlAvailable: boolean; canPublicNote: boolean; - canEditNote: boolean; canInvite: boolean; inviteLimit: number; inviteLimitCycle: number; @@ -52,7 +52,6 @@ export const DEFAULT_POLICIES: RolePolicies = { gtlAvailable: true, ltlAvailable: true, canPublicNote: true, - canEditNote: true, canInvite: false, inviteLimit: 0, inviteLimitCycle: 60 * 24 * 7, @@ -104,6 +103,7 @@ export class RoleService implements OnApplicationShutdown { private globalEventService: GlobalEventService, private idService: IdService, private moderationLogService: ModerationLogService, + private redisTimelineService: RedisTimelineService, ) { //this.onMessage = this.onMessage.bind(this); @@ -298,7 +298,6 @@ export class RoleService implements OnApplicationShutdown { gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)), ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), - canEditNote: calc('canEditNote', vs => vs.some(v => v === true)), canInvite: calc('canInvite', vs => vs.some(v => v === true)), inviteLimit: calc('inviteLimit', vs => Math.max(...vs)), inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)), @@ -475,12 +474,7 @@ export class RoleService implements OnApplicationShutdown { const redisPipeline = this.redisClient.pipeline(); for (const role of roles) { - redisPipeline.xadd( - `roleTimeline:${role.id}`, - 'MAXLEN', '~', '1000', - '*', - 'note', note.id); - + this.redisTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline); this.globalEventService.publishRoleTimelineStream(role.id, 'note', note); } diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 37031e341e4f20cc223d00cd8e5fc4655089bef3..087dfd92147ceba7ad1fce485248e2f89e170ed0 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -11,7 +11,7 @@ import type { MiBlocking } from '@/models/Blocking.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/_.js'; +import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListMembershipsRepository } from '@/models/_.js'; import Logger from '@/logger.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -38,8 +38,8 @@ export class UserBlockingService implements OnModuleInit { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private cacheService: CacheService, private userEntityService: UserEntityService, @@ -149,7 +149,7 @@ export class UserBlockingService implements OnModuleInit { }); for (const userList of userLists) { - await this.userListJoiningsRepository.delete({ + await this.userListMembershipsRepository.delete({ userListId: userList.id, userId: user.id, }); diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 230f6ef261b8997ead7f5c8012ed2e542bfb08ec..beffcc2e9c9720678a00d5eadb1a02d7eaed9f93 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -123,7 +123,11 @@ export class UserFollowingService implements OnModuleInit { // フォãƒãƒ¯ãƒ¼ãŒBotã§ã‚ã‚Šã€ãƒ•ã‚©ãƒãƒ¼å¯¾è±¡ãŒBotã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ã«æ…Žé‡ã§ã‚ã‚‹ or // フォãƒãƒ¯ãƒ¼ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã§ã‚ã‚Šã€ãƒ•ã‚©ãƒãƒ¼å¯¾è±¡ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã§ã‚ã‚‹ // 上記ã®ã„ãšã‚Œã‹ã«å½“ã¦ã¯ã¾ã‚‹å ´åˆã¯ã™ãフォãƒãƒ¼ã›ãšã«ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’発行ã—ã¦ãŠã - if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee))) { + if ( + followee.isLocked || + (followeeProfile.carefulBot && follower.isBot) || + (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') + ) { let autoAccept = false; // éµã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã‚ã£ã¦ã‚‚ã€æ—¢ã«ãƒ•ã‚©ãƒãƒ¼ã•ã‚Œã¦ã„ãŸå ´åˆã¯ã‚¹ãƒ«ãƒ¼ diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index 93dc5edbbafe8214157f0c03ba6946ce075c26f0..5b4e7a711eca30c9eb5659e1591d2e8c2bf906a0 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -5,10 +5,10 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { UserListJoiningsRepository } from '@/models/_.js'; +import type { UserListMembershipsRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import type { MiUserList } from '@/models/UserList.js'; -import type { MiUserListJoining } from '@/models/UserListJoining.js'; +import type { MiUserListMembership } from '@/models/UserListMembership.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -33,8 +33,8 @@ export class UserListService implements OnApplicationShutdown { @Inject(DI.redisForSub) private redisForSub: Redis.Redis, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private userEntityService: UserEntityService, private idService: IdService, @@ -46,7 +46,7 @@ export class UserListService implements OnApplicationShutdown { this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60, // 1m - fetcher: (key) => this.userListJoiningsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))), + fetcher: (key) => this.userListMembershipsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))), toRedisConverter: (value) => JSON.stringify(Array.from(value)), fromRedisConverter: (value) => new Set(JSON.parse(value)), }); @@ -85,19 +85,20 @@ export class UserListService implements OnApplicationShutdown { @bindThis public async addMember(target: MiUser, list: MiUserList, me: MiUser) { - const currentCount = await this.userListJoiningsRepository.countBy({ + const currentCount = await this.userListMembershipsRepository.countBy({ userListId: list.id, }); if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { throw new UserListService.TooManyUsersError(); } - await this.userListJoiningsRepository.insert({ + await this.userListMembershipsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), userId: target.id, userListId: list.id, - } as MiUserListJoining); + userListUserId: list.userId, + } as MiUserListMembership); this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id }); this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target)); @@ -113,7 +114,7 @@ export class UserListService implements OnApplicationShutdown { @bindThis public async removeMember(target: MiUser, list: MiUserList) { - await this.userListJoiningsRepository.delete({ + await this.userListMembershipsRepository.delete({ userId: target.id, userListId: list.id, }); @@ -122,6 +123,24 @@ export class UserListService implements OnApplicationShutdown { this.globalEventService.publishUserListStream(list.id, 'userRemoved', await this.userEntityService.pack(target)); } + @bindThis + public async updateMembership(target: MiUser, list: MiUserList, options: { withReplies?: boolean }) { + const membership = await this.userListMembershipsRepository.findOneBy({ + userId: target.id, + userListId: list.id, + }); + + if (membership == null) { + throw new Error('User is not a member of the list'); + } + + await this.userListMembershipsRepository.update({ + id: membership.id, + }, { + withReplies: options.withReplies, + }); + } + @bindThis public dispose(): void { this.redisForSub.off('message', this.onMessage); diff --git a/packages/backend/src/core/UserService.ts b/packages/backend/src/core/UserService.ts new file mode 100644 index 0000000000000000000000000000000000000000..d16e1be61513e0e77b774a96bbe790a0481d3772 --- /dev/null +++ b/packages/backend/src/core/UserService.ts @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { FollowingsRepository, UsersRepository } from '@/models/_.js'; +import type { MiUser } from '@/models/User.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; + +@Injectable() +export class UserService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + ) { + } + + @bindThis + public async updateLastActiveDate(user: MiUser): Promise<void> { + if (user.isHibernated) { + const result = await this.usersRepository.createQueryBuilder().update() + .set({ + lastActiveDate: new Date(), + }) + .where('id = :id', { id: user.id }) + .returning('*') + .execute() + .then((response) => { + return response.raw[0]; + }); + const wokeUp = result.isHibernated; + if (wokeUp) { + this.usersRepository.update(user.id, { + isHibernated: false, + }); + this.followingsRepository.update({ + followerId: user.id, + }, { + isFollowerHibernated: false, + }); + } + } else { + this.usersRepository.update(user.id, { + lastActiveDate: new Date(), + }); + } + } +} diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index a024286b480719c754821c5f23db298ccebc3f66..abe4aafd6e348914157e88138da38ed95614f7b5 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -17,6 +17,7 @@ import type { MiNoteReaction } from '@/models/NoteReaction.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { isNotNull } from '@/misc/is-not-null.js'; +import { DebounceLoader } from '@/misc/loader.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { ReactionService } from '../ReactionService.js'; @@ -29,6 +30,7 @@ export class NoteEntityService implements OnModuleInit { private driveFileEntityService: DriveFileEntityService; private customEmojiService: CustomEmojiService; private reactionService: ReactionService; + private noteLoader = new DebounceLoader(this.findNoteOrFail); constructor( private moduleRef: ModuleRef, @@ -98,13 +100,13 @@ export class NoteEntityService implements OnModuleInit { } else if (meId === packedNote.userId) { hide = false; } else if (packedNote.reply && (meId === packedNote.reply.userId)) { - // 自分ã®æŠ•ç¨¿ã«å¯¾ã™ã‚‹ãƒªãƒ—ライ + // 自分ã®æŠ•ç¨¿ã«å¯¾ã™ã‚‹ãƒªãƒ—ライ hide = false; } else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) { - // 自分ã¸ã®ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ + // 自分ã¸ã®ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ hide = false; } else { - // フォãƒãƒ¯ãƒ¼ã‹ã©ã†ã‹ + // フォãƒãƒ¯ãƒ¼ã‹ã©ã†ã‹ const isFollowing = await this.followingsRepository.exist({ where: { followeeId: packedNote.userId, @@ -285,7 +287,7 @@ export class NoteEntityService implements OnModuleInit { }, options); const meId = me ? me.id : null; - const note = typeof src === 'object' ? src : await this.notesRepository.findOneOrFail({ where: { id: src }, relations: ['user'] }); + const note = typeof src === 'object' ? src : await this.noteLoader.load(src); const host = note.userHost; let text = note.text; @@ -308,7 +310,6 @@ export class NoteEntityService implements OnModuleInit { const packed: Packed<'Note'> = await awaitAll({ id: note.id, createdAt: note.createdAt.toISOString(), - updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined, userId: note.userId, user: this.userEntityService.pack(note.user ?? note.userId, me, { detail: false, @@ -453,17 +454,10 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> { - // 指定ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŒ‡å®šã—ãŸãƒŽãƒ¼ãƒˆã®ãƒªãƒŽãƒ¼ãƒˆãŒã„ãã¤ã‚ã‚‹ã‹æ•°ãˆã‚‹ - const query = this.notesRepository.createQueryBuilder('note') - .where('note.userId = :userId', { userId }) - .andWhere('note.renoteId = :renoteId', { renoteId }); - - // 指定ã—ãŸæŠ•ç¨¿ã‚’除ã - if (excludeNoteId) { - query.andWhere('note.id != :excludeNoteId', { excludeNoteId }); - } - - return await query.getCount(); + private findNoteOrFail(id: string): Promise<MiNote> { + return this.notesRepository.findOneOrFail({ + where: { id }, + relations: ['user'], + }); } } diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts index 23e82561d670fbf39d6cf1ffb381cc8d518146bc..79375a700838dce997fa46ada2030670fe4c7bde 100644 --- a/packages/backend/src/core/entities/RoleEntityService.ts +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -33,9 +33,10 @@ export class RoleEntityService { const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign') .where('assign.roleId = :roleId', { roleId: role.id }) - .andWhere(new Brackets(qb => { qb - .where('assign.expiresAt IS NULL') - .orWhere('assign.expiresAt > :now', { now: new Date() }); + .andWhere(new Brackets(qb => { + qb + .where('assign.expiresAt IS NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); })) .getCount(); diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index a47b3d51ac2bab3cde73a6c0e30807a7519e47a1..ee67634da5000fd49c4f8b9161e9d4262ead1ad1 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -146,64 +146,76 @@ export class UserEntityService implements OnModuleInit { @bindThis public async getRelation(me: MiUser['id'], target: MiUser['id']) { - const following = await this.followingsRepository.findOneBy({ - followerId: me, - followeeId: target, - }); - return awaitAll({ - id: target, + const [ following, - isFollowing: following != null, - isFollowed: this.followingsRepository.count({ + isFollowed, + hasPendingFollowRequestFromYou, + hasPendingFollowRequestToYou, + isBlocking, + isBlocked, + isMuted, + isRenoteMuted, + ] = await Promise.all([ + this.followingsRepository.findOneBy({ + followerId: me, + followeeId: target, + }), + this.followingsRepository.exist({ where: { followerId: target, followeeId: me, }, - take: 1, - }).then(n => n > 0), - hasPendingFollowRequestFromYou: this.followRequestsRepository.count({ + }), + this.followRequestsRepository.exist({ where: { followerId: me, followeeId: target, }, - take: 1, - }).then(n => n > 0), - hasPendingFollowRequestToYou: this.followRequestsRepository.count({ + }), + this.followRequestsRepository.exist({ where: { followerId: target, followeeId: me, }, - take: 1, - }).then(n => n > 0), - isBlocking: this.blockingsRepository.count({ + }), + this.blockingsRepository.exist({ where: { blockerId: me, blockeeId: target, }, - take: 1, - }).then(n => n > 0), - isBlocked: this.blockingsRepository.count({ + }), + this.blockingsRepository.exist({ where: { blockerId: target, blockeeId: me, }, - take: 1, - }).then(n => n > 0), - isMuted: this.mutingsRepository.count({ + }), + this.mutingsRepository.exist({ where: { muterId: me, muteeId: target, }, - take: 1, - }).then(n => n > 0), - isRenoteMuted: this.renoteMutingsRepository.count({ + }), + this.renoteMutingsRepository.exist({ where: { muterId: me, muteeId: target, }, - take: 1, - }).then(n => n > 0), - }); + }), + ]); + + return { + id: target, + following, + isFollowing: following != null, + isFollowed, + hasPendingFollowRequestFromYou, + hasPendingFollowRequestToYou, + isBlocking, + isBlocked, + isMuted, + isRenoteMuted, + }; } @bindThis @@ -290,24 +302,6 @@ export class UserEntityService implements OnModuleInit { const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src }); - // migration - if (user.avatarId != null && user.avatarUrl === null) { - const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId }); - user.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar'); - this.usersRepository.update(user.id, { - avatarUrl: user.avatarUrl, - avatarBlurhash: avatar.blurhash, - }); - } - if (user.bannerId != null && user.bannerUrl === null) { - const banner = await this.driveFilesRepository.findOneByOrFail({ id: user.bannerId }); - user.bannerUrl = this.driveFileEntityService.getPublicUrl(banner); - this.usersRepository.update(user.id, { - bannerUrl: user.bannerUrl, - bannerBlurhash: banner.blurhash, - }); - } - const meId = me ? me.id : null; const isMe = meId === user.id; const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; @@ -487,6 +481,7 @@ export class UserEntityService implements OnModuleInit { isMuted: relation.isMuted, isRenoteMuted: relation.isRenoteMuted, notify: relation.following?.notify ?? 'none', + withReplies: relation.following?.withReplies ?? false, } : {}), } as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>; diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts index a7f28851943f935d7eec85ffa9c6abffcab1a4d0..06b6e852b1463cf531275e13962519d891340324 100644 --- a/packages/backend/src/core/entities/UserListEntityService.ts +++ b/packages/backend/src/core/entities/UserListEntityService.ts @@ -5,11 +5,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js'; +import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/Blocking.js'; import type { MiUserList } from '@/models/UserList.js'; import { bindThis } from '@/decorators.js'; +import { UserEntityService } from './UserEntityService.js'; @Injectable() export class UserListEntityService { @@ -17,8 +18,10 @@ export class UserListEntityService { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, + + private userEntityService: UserEntityService, ) { } @@ -28,7 +31,7 @@ export class UserListEntityService { ): Promise<Packed<'UserList'>> { const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src }); - const users = await this.userListJoiningsRepository.findBy({ + const users = await this.userListMembershipsRepository.findBy({ userListId: userList.id, }); @@ -40,5 +43,18 @@ export class UserListEntityService { isPublic: userList.isPublic, }; } + + @bindThis + public async packMembershipsMany( + memberships: MiUserListMembership[], + ) { + return Promise.all(memberships.map(async x => ({ + id: x.id, + createdAt: x.createdAt.toISOString(), + userId: x.userId, + user: await this.userEntityService.pack(x.userId), + withReplies: x.withReplies, + }))); + } } diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 72ec98cebe890192ea63d595eaafdff9989ecf6b..edcdd21d606ac748d68793e0f1559c869cf7dbf9 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -10,6 +10,7 @@ export const DI = { redis: Symbol('redis'), redisForPub: Symbol('redisForPub'), redisForSub: Symbol('redisForSub'), + redisForTimelines: Symbol('redisForTimelines'), //#region Repositories usersRepository: Symbol('usersRepository'), @@ -30,7 +31,7 @@ export const DI = { userPublickeysRepository: Symbol('userPublickeysRepository'), userListsRepository: Symbol('userListsRepository'), userListFavoritesRepository: Symbol('userListFavoritesRepository'), - userListJoiningsRepository: Symbol('userListJoiningsRepository'), + userListMembershipsRepository: Symbol('userListMembershipsRepository'), userNotePiningsRepository: Symbol('userNotePiningsRepository'), userIpsRepository: Symbol('userIpsRepository'), usedUsernamesRepository: Symbol('usedUsernamesRepository'), @@ -63,7 +64,6 @@ export const DI = { promoNotesRepository: Symbol('promoNotesRepository'), promoReadsRepository: Symbol('promoReadsRepository'), relaysRepository: Symbol('relaysRepository'), - mutedNotesRepository: Symbol('mutedNotesRepository'), channelsRepository: Symbol('channelsRepository'), channelFollowingsRepository: Symbol('channelFollowingsRepository'), channelFavoritesRepository: Symbol('channelFavoritesRepository'), diff --git a/packages/backend/src/misc/is-user-related.ts b/packages/backend/src/misc/is-user-related.ts index edd65a3c1cc1370977985f91fd3c5f985aece476..6efb1194d3bba974b5032635070e0442190c7162 100644 --- a/packages/backend/src/misc/is-user-related.ts +++ b/packages/backend/src/misc/is-user-related.ts @@ -3,16 +3,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export function isUserRelated(note: any, userIds: Set<string>): boolean { - if (userIds.has(note.userId)) { +export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean { + if (userIds.has(note.userId) && !ignoreAuthor) { return true; } - if (note.reply != null && userIds.has(note.reply.userId)) { + if (note.reply != null && note.reply.userId !== note.userId && userIds.has(note.reply.userId)) { return true; } - if (note.renote != null && userIds.has(note.renote.userId)) { + if (note.renote != null && note.renote.userId !== note.userId && userIds.has(note.renote.userId)) { return true; } diff --git a/packages/backend/src/misc/loader.ts b/packages/backend/src/misc/loader.ts new file mode 100644 index 0000000000000000000000000000000000000000..25f7b54d31c62c6c1e8d7703fe81bac4b353d8e6 --- /dev/null +++ b/packages/backend/src/misc/loader.ts @@ -0,0 +1,52 @@ +export type FetchFunction<K, V> = (key: K) => Promise<V>; + +type ResolveReject<V> = Parameters<ConstructorParameters<typeof Promise<V>>[0]>; + +type ResolverPair<V> = { + resolve: ResolveReject<V>[0]; + reject: ResolveReject<V>[1]; +}; + +export class DebounceLoader<K, V> { + private resolverMap = new Map<K, ResolverPair<V>>(); + private promiseMap = new Map<K, Promise<V>>(); + private resolvedPromise = Promise.resolve(); + constructor(private loadFn: FetchFunction<K, V>) {} + + public load(key: K): Promise<V> { + const promise = this.promiseMap.get(key); + if (typeof promise !== 'undefined') { + return promise; + } + + const isFirst = this.promiseMap.size === 0; + const newPromise = new Promise<V>((resolve, reject) => { + this.resolverMap.set(key, { resolve, reject }); + }); + this.promiseMap.set(key, newPromise); + + if (isFirst) { + this.enqueueDebouncedLoadJob(); + } + + return newPromise; + } + + private runDebouncedLoad(): void { + const resolvers = [...this.resolverMap]; + this.resolverMap.clear(); + this.promiseMap.clear(); + + for (const [key, { resolve, reject }] of resolvers) { + this.loadFn(key).then(resolve, reject); + } + } + + private enqueueDebouncedLoadJob(): void { + this.resolvedPromise.then(() => { + process.nextTick(() => { + this.runDebouncedLoad(); + }); + }); + } +} diff --git a/packages/backend/src/models/Following.ts b/packages/backend/src/models/Following.ts index 8c9f965fadd4093d98b5c9d27a1a2ff8a5ab0d75..607538b1e79cf50e5df68e3f320f59315e36eb10 100644 --- a/packages/backend/src/models/Following.ts +++ b/packages/backend/src/models/Following.ts @@ -9,6 +9,7 @@ import { MiUser } from './User.js'; @Entity('following') @Index(['followerId', 'followeeId'], { unique: true }) +@Index(['followeeId', 'followerHost', 'isFollowerHibernated']) export class MiFollowing { @PrimaryColumn(id()) public id: string; @@ -45,6 +46,17 @@ export class MiFollowing { @JoinColumn() public follower: MiUser | null; + @Column('boolean', { + default: false, + }) + public isFollowerHibernated: boolean; + + // タイムラインã«ãã®äººã®ãƒªãƒ—ライã¾ã§å«ã‚ã‚‹ã‹ã©ã†ã‹ + @Column('boolean', { + default: false, + }) + public withReplies: boolean; + @Index() @Column('varchar', { length: 32, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index e69bef8e9845f5568b2ed45143e03ecd28016ecf..d2bd0c26e99d9fb04bce90e2e8551bd53ad0fca5 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -335,6 +335,18 @@ export class MiMeta { }) public feedbackUrl: string | null; + @Column('varchar', { + length: 1024, + nullable: true, + }) + public impressumUrl: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public privacyPolicyUrl: string | null; + @Column('varchar', { length: 8192, nullable: true, @@ -471,4 +483,29 @@ export class MiMeta { length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }', }) public preservedUsernames: string[]; + + @Column('integer', { + default: 300, + }) + public perLocalUserUserTimelineCacheMax: number; + + @Column('integer', { + default: 100, + }) + public perRemoteUserUserTimelineCacheMax: number; + + @Column('integer', { + default: 300, + }) + public perUserHomeTimelineCacheMax: number; + + @Column('integer', { + default: 300, + }) + public perUserListTimelineCacheMax: number; + + @Column('integer', { + default: 0, + }) + public notesPerOneAd: number; } diff --git a/packages/backend/src/models/MutedNote.ts b/packages/backend/src/models/MutedNote.ts deleted file mode 100644 index 89a678a2a77805c4a18eb2296af418b351bffca3..0000000000000000000000000000000000000000 --- a/packages/backend/src/models/MutedNote.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; -import { mutedNoteReasons } from '@/types.js'; -import { id } from './util/id.js'; -import { MiNote } from './Note.js'; -import { MiUser } from './User.js'; - -@Entity('muted_note') -@Index(['noteId', 'userId'], { unique: true }) -export class MiMutedNote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The note ID.', - }) - public noteId: MiNote['id']; - - @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: MiNote | null; - - @Index() - @Column({ - ...id(), - comment: 'The user ID.', - }) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - /** - * ミュートã•ã‚ŒãŸç†ç”±ã€‚ - */ - @Index() - @Column('enum', { - enum: mutedNoteReasons, - comment: 'The reason of the MutedNote.', - }) - public reason: typeof mutedNoteReasons[number]; -} diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index f396a0cd7a83c3a50da88411d75092ef39daeac5..3e2adf4d82e633830002e0b379e3d91d257a5849 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -18,17 +18,11 @@ export class MiNote { @PrimaryColumn(id()) public id: string; - @Index() @Column('timestamp with time zone', { comment: 'The created date of the Note.', }) public createdAt: Date; - @Column('timestamp with time zone', { - default: null, - }) - public updatedAt: Date | null; - @Index() @Column({ ...id(), @@ -144,11 +138,6 @@ export class MiNote { }) public url: string | null; - @Column('integer', { - default: 0, select: false, - }) - public score: number; - @Index() @Column({ ...id(), @@ -156,7 +145,6 @@ export class MiNote { }) public fileIds: MiDriveFile['id'][]; - @Index() @Column('varchar', { length: 256, array: true, default: '{}', }) diff --git a/packages/backend/src/models/NoteReaction.ts b/packages/backend/src/models/NoteReaction.ts index 7c08d31c6d7866434609007b7931c1e16680c874..43323f8a438c92d06009d7bf023a194695807f90 100644 --- a/packages/backend/src/models/NoteReaction.ts +++ b/packages/backend/src/models/NoteReaction.ts @@ -14,7 +14,6 @@ export class MiNoteReaction { @PrimaryColumn(id()) public id: string; - @Index() @Column('timestamp with time zone', { comment: 'The created date of the NoteReaction.', }) diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 766e7ce21c70656791683cf857af821d6b7a9efa..9efd6841b180e3674c6b4e95e1cb88be1a8ab47e 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -5,7 +5,7 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMutedNote, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListJoining, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js'; +import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -117,9 +117,9 @@ const $userListFavoritesRepository: Provider = { inject: [DI.db], }; -const $userListJoiningsRepository: Provider = { - provide: DI.userListJoiningsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserListJoining), +const $userListMembershipsRepository: Provider = { + provide: DI.userListMembershipsRepository, + useFactory: (db: DataSource) => db.getRepository(MiUserListMembership), inject: [DI.db], }; @@ -315,12 +315,6 @@ const $relaysRepository: Provider = { inject: [DI.db], }; -const $mutedNotesRepository: Provider = { - provide: DI.mutedNotesRepository, - useFactory: (db: DataSource) => db.getRepository(MiMutedNote), - inject: [DI.db], -}; - const $channelsRepository: Provider = { provide: DI.channelsRepository, useFactory: (db: DataSource) => db.getRepository(MiChannel), @@ -421,7 +415,7 @@ const $userMemosRepository: Provider = { $userPublickeysRepository, $userListsRepository, $userListFavoritesRepository, - $userListJoiningsRepository, + $userListMembershipsRepository, $userNotePiningsRepository, $userIpsRepository, $usedUsernamesRepository, @@ -454,7 +448,6 @@ const $userMemosRepository: Provider = { $promoNotesRepository, $promoReadsRepository, $relaysRepository, - $mutedNotesRepository, $channelsRepository, $channelFollowingsRepository, $channelFavoritesRepository, @@ -488,7 +481,7 @@ const $userMemosRepository: Provider = { $userPublickeysRepository, $userListsRepository, $userListFavoritesRepository, - $userListJoiningsRepository, + $userListMembershipsRepository, $userNotePiningsRepository, $userIpsRepository, $usedUsernamesRepository, @@ -521,7 +514,6 @@ const $userMemosRepository: Provider = { $promoNotesRepository, $promoReadsRepository, $relaysRepository, - $mutedNotesRepository, $channelsRepository, $channelFollowingsRepository, $channelFavoritesRepository, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index b040d302ce7c662f2de77889174058da3875c51b..4d961c4290b9492656027b13060ded7bf264821b 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -187,6 +187,11 @@ export class MiUser { }) public isExplorable: boolean; + @Column('boolean', { + default: false, + }) + public isHibernated: boolean; + // アカウントãŒå‰Šé™¤ã•ã‚ŒãŸã‹ã©ã†ã‹ã®ãƒ•ãƒ©ã‚°ã ãŒã€å®Œå…¨ã«å‰Šé™¤ã•ã‚Œã‚‹éš›ã¯ç‰©ç†å‰Šé™¤ãªã®ã§å®Ÿè³ªå‰Šé™¤ã•ã‚Œã‚‹ã¾ã§ã®ã€Œå‰Šé™¤ãŒé€²è¡Œã—ã¦ã„ã‚‹ã‹ã©ã†ã‹ã€ã®ãƒ•ãƒ©ã‚° @Column('boolean', { default: false, diff --git a/packages/backend/src/models/UserListJoining.ts b/packages/backend/src/models/UserListMembership.ts similarity index 69% rename from packages/backend/src/models/UserListJoining.ts rename to packages/backend/src/models/UserListMembership.ts index 4918f2f700010562e5c7d21579893f8f65a2f95f..f57f9ac33d5bfd576cfd42a5baf6b05840a7b32c 100644 --- a/packages/backend/src/models/UserListJoining.ts +++ b/packages/backend/src/models/UserListMembership.ts @@ -8,14 +8,14 @@ import { id } from './util/id.js'; import { MiUser } from './User.js'; import { MiUserList } from './UserList.js'; -@Entity('user_list_joining') +@Entity('user_list_membership') @Index(['userId', 'userListId'], { unique: true }) -export class MiUserListJoining { +export class MiUserListMembership { @PrimaryColumn(id()) public id: string; @Column('timestamp with time zone', { - comment: 'The created date of the UserListJoining.', + comment: 'The created date of the UserListMembership.', }) public createdAt: Date; @@ -44,4 +44,17 @@ export class MiUserListJoining { }) @JoinColumn() public userList: MiUserList | null; + + // タイムラインã«ãã®äººã®ãƒªãƒ—ライã¾ã§å«ã‚ã‚‹ã‹ã©ã†ã‹ + @Column('boolean', { + default: false, + }) + public withReplies: boolean; + + //#region Denormalized fields + @Column({ + ...id(), + }) + public userListUserId: MiUser['id']; + //#endregion } diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index 6be7bd0df6ba6da245611ce699f0d5f2a9b4c517..f974f95ed89fcc1c8f667da2699b6a611222847c 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -28,7 +28,6 @@ import { MiHashtag } from '@/models/Hashtag.js'; import { MiInstance } from '@/models/Instance.js'; import { MiMeta } from '@/models/Meta.js'; import { MiModerationLog } from '@/models/ModerationLog.js'; -import { MiMutedNote } from '@/models/MutedNote.js'; import { MiMuting } from '@/models/Muting.js'; import { MiRenoteMuting } from '@/models/RenoteMuting.js'; import { MiNote } from '@/models/Note.js'; @@ -53,7 +52,7 @@ import { MiUser } from '@/models/User.js'; import { MiUserIp } from '@/models/UserIp.js'; import { MiUserKeypair } from '@/models/UserKeypair.js'; import { MiUserList } from '@/models/UserList.js'; -import { MiUserListJoining } from '@/models/UserListJoining.js'; +import { MiUserListMembership } from '@/models/UserListMembership.js'; import { MiUserNotePining } from '@/models/UserNotePining.js'; import { MiUserPending } from '@/models/UserPending.js'; import { MiUserProfile } from '@/models/UserProfile.js'; @@ -96,7 +95,6 @@ export { MiInstance, MiMeta, MiModerationLog, - MiMutedNote, MiMuting, MiRenoteMuting, MiNote, @@ -122,7 +120,7 @@ export { MiUserKeypair, MiUserList, MiUserListFavorite, - MiUserListJoining, + MiUserListMembership, MiUserNotePining, MiUserPending, MiUserProfile, @@ -163,7 +161,6 @@ export type HashtagsRepository = Repository<MiHashtag>; export type InstancesRepository = Repository<MiInstance>; export type MetasRepository = Repository<MiMeta>; export type ModerationLogsRepository = Repository<MiModerationLog>; -export type MutedNotesRepository = Repository<MiMutedNote>; export type MutingsRepository = Repository<MiMuting>; export type RenoteMutingsRepository = Repository<MiRenoteMuting>; export type NotesRepository = Repository<MiNote>; @@ -189,7 +186,7 @@ export type UserIpsRepository = Repository<MiUserIp>; export type UserKeypairsRepository = Repository<MiUserKeypair>; export type UserListsRepository = Repository<MiUserList>; export type UserListFavoritesRepository = Repository<MiUserListFavorite>; -export type UserListJoiningsRepository = Repository<MiUserListJoining>; +export type UserListMembershipsRepository = Repository<MiUserListMembership>; export type UserNotePiningsRepository = Repository<MiUserNotePining>; export type UserPendingsRepository = Repository<MiUserPending>; export type UserProfilesRepository = Repository<MiUserProfile>; diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index ad0cb3c45d6a0e6e001360b7ac2a283bfc896bce..2caf0d0c3d9f75684cb7b370cac70f91690c7dd3 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -17,11 +17,6 @@ export const packedNoteSchema = { optional: false, nullable: false, format: 'date-time', }, - updatedAt: { - type: 'string', - optional: true, nullable: true, - format: 'date-time', - }, deletedAt: { type: 'string', optional: true, nullable: true, diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 0181ea50e8dd6834b28598b96ad75335281a79f2..57d2d976ff7a5727d5d2bb2e338a8dc2ed60ff6c 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -277,6 +277,10 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'string', nullable: false, optional: true, }, + withReplies: { + type: 'boolean', + nullable: false, optional: true, + }, //#endregion }, } as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 10126eab2bbdf0cb76692f309992ffc675d082e9..d4c6ad82ce48857f5f1be6fea7ea2a988a740524 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -36,7 +36,6 @@ import { MiHashtag } from '@/models/Hashtag.js'; import { MiInstance } from '@/models/Instance.js'; import { MiMeta } from '@/models/Meta.js'; import { MiModerationLog } from '@/models/ModerationLog.js'; -import { MiMutedNote } from '@/models/MutedNote.js'; import { MiMuting } from '@/models/Muting.js'; import { MiRenoteMuting } from '@/models/RenoteMuting.js'; import { MiNote } from '@/models/Note.js'; @@ -62,7 +61,7 @@ import { MiUserIp } from '@/models/UserIp.js'; import { MiUserKeypair } from '@/models/UserKeypair.js'; import { MiUserList } from '@/models/UserList.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js'; -import { MiUserListJoining } from '@/models/UserListJoining.js'; +import { MiUserListMembership } from '@/models/UserListMembership.js'; import { MiUserNotePining } from '@/models/UserNotePining.js'; import { MiUserPending } from '@/models/UserPending.js'; import { MiUserProfile } from '@/models/UserProfile.js'; @@ -138,7 +137,7 @@ export const entities = [ MiUserPublickey, MiUserList, MiUserListFavorite, - MiUserListJoining, + MiUserListMembership, MiUserNotePining, MiUserSecurityKey, MiUsedUsername, @@ -174,7 +173,6 @@ export const entities = [ MiPromoNote, MiPromoRead, MiRelay, - MiMutedNote, MiChannel, MiChannelFollowing, MiChannelFavorite, diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index f0453f7054009c8437a59f4adc13290ed646c46c..e252c5d8a1444142d274558b00eb983831f86ed2 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, LessThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennasRepository, MutedNotesRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/_.js'; +import type { AntennasRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; @@ -25,9 +25,6 @@ export class CleanProcessorService { @Inject(DI.userIpsRepository) private userIpsRepository: UserIpsRepository, - @Inject(DI.mutedNotesRepository) - private mutedNotesRepository: MutedNotesRepository, - @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, @@ -48,16 +45,6 @@ export class CleanProcessorService { createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))), }); - this.mutedNotesRepository.delete({ - id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))), - reason: 'word', - }); - - this.mutedNotesRepository.delete({ - id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))), - reason: 'word', - }); - // 使ã‚ã‚Œã¦ãªã„アンテナをåœæ¢ if (this.config.deactivateAntennaThreshold > 0) { this.antennasRepository.update({ diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts index f941fb6e858cdb50b3a4b38cadad4c10317c55eb..a0afbee3baa1357604236e70d891863ce4d63a1d 100644 --- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts @@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { format as DateFormat } from 'date-fns'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennasRepository, UsersRepository, UserListJoiningsRepository, MiUser } from '@/models/_.js'; +import type { AntennasRepository, UsersRepository, UserListMembershipsRepository, MiUser } from '@/models/_.js'; import Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { bindThis } from '@/decorators.js'; @@ -29,8 +29,8 @@ export class ExportAntennasProcessorService { @Inject(DI.antennasRepository) private antennsRepository: AntennasRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private driveService: DriveService, private utilityService: UtilityService, @@ -65,9 +65,9 @@ export class ExportAntennasProcessorService { for (const [index, antenna] of antennas.entries()) { let users: MiUser[] | undefined; if (antenna.userListId !== null) { - const joinings = await this.userListJoiningsRepository.findBy({ userListId: antenna.userListId }); + const memberships = await this.userListMembershipsRepository.findBy({ userListId: antenna.userListId }); users = await this.usersRepository.findBy({ - id: In(joinings.map(j => j.userId)), + id: In(memberships.map(j => j.userId)), }); } write(JSON.stringify({ diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index 7baaa7081a86724dd3b6219f3f685194b7f97fd1..a3f9441dc28247fc7ee649c1506d6dda615e89fd 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { UserListJoiningsRepository, UserListsRepository, UsersRepository } from '@/models/_.js'; +import type { UserListMembershipsRepository, UserListsRepository, UsersRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; @@ -29,8 +29,8 @@ export class ExportUserListsProcessorService { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private utilityService: UtilityService, private driveService: DriveService, @@ -61,9 +61,9 @@ export class ExportUserListsProcessorService { const stream = fs.createWriteStream(path, { flags: 'a' }); for (const list of lists) { - const joinings = await this.userListJoiningsRepository.findBy({ userListId: list.id }); + const memberships = await this.userListMembershipsRepository.findBy({ userListId: list.id }); const users = await this.usersRepository.findBy({ - id: In(joinings.map(j => j.userId)), + id: In(memberships.map(j => j.userId)), }); for (const u of users) { diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index 60a0d1605f11fd6233ab9c4ab329cd11fa7b40aa..9be36a9d0d1ed7b5b5537ae85d23ba4a7b834cc5 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, DriveFilesRepository, UserListJoiningsRepository, UserListsRepository } from '@/models/_.js'; +import type { UsersRepository, DriveFilesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; @@ -33,8 +33,8 @@ export class ImportUserListsProcessorService { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private utilityService: UtilityService, private idService: IdService, @@ -99,7 +99,7 @@ export class ImportUserListsProcessorService { target = await this.remoteUserResolveService.resolveUser(username, host); } - if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue; + if (await this.userListMembershipsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue; this.userListService.addMember(target, list!, user); } catch (e) { diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 2428fa279233cb418a55a7698f74daf7b961588e..a7f6f82daff66d4cbaf0bbbe1b74cbbb61344c66 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -379,9 +379,10 @@ export class ActivityPubServerService { if (page) { const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId) .andWhere('note.userId = :userId', { userId: user.id }) - .andWhere(new Brackets(qb => { qb - .where('note.visibility = \'public\'') - .orWhere('note.visibility = \'home\''); + .andWhere(new Brackets(qb => { + qb + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); })) .andWhere('note.localOnly = FALSE'); diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 79f130dabe4abac6242142a5b50e0454cbc08d6f..79b0a57f2b2c410cc9ab447027f388515b42287d 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -102,6 +102,8 @@ export class NodeinfoServerService { }, langs: meta.langs, tosUrl: meta.termsOfServiceUrl, + privacyPolicyUrl: meta.privacyPolicyUrl, + impressumUrl: meta.impressumUrl, repositoryUrl: meta.repositoryUrl, feedbackUrl: meta.feedbackUrl, disableRegistration: meta.disableRegistration, @@ -133,7 +135,11 @@ export class NodeinfoServerService { .type( 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"', ) - .header('Cache-Control', 'public, max-age=600'); + .header('Cache-Control', 'public, max-age=600') + .header('Access-Control-Allow-Headers', 'Accept') + .header('Access-Control-Allow-Methods', 'GET, OPTIONS') + .header('Access-Control-Allow-Origin', '*') + .header('Access-Control-Expose-Headers', 'Vary'); return { version: '2.1', ...base }; }); @@ -146,7 +152,11 @@ export class NodeinfoServerService { .type( 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"', ) - .header('Cache-Control', 'public, max-age=600'); + .header('Cache-Control', 'public, max-age=600') + .header('Access-Control-Allow-Headers', 'Accept') + .header('Access-Control-Allow-Methods', 'GET, OPTIONS') + .header('Access-Control-Allow-Origin', '*') + .header('Access-Control-Expose-Headers', 'Vary'); return { version: '2.0', ...base }; }); diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 0e4a5ece3ec91fd00c686112f6e51689a32934bf..e598b91e51813b20b37e482fe0e0eaf5dd9cf51f 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -199,10 +199,10 @@ export class ServerService implements OnApplicationShutdown { includeSecrets: true, })); - reply.code(200); - return 'Verify succeeded!'; + reply.code(200).send('Verification succeeded! メールアドレスã®èªè¨¼ã«æˆåŠŸã—ã¾ã—ãŸã€‚'); + return; } else { - reply.code(404); + reply.code(404).send('Verification failed. Please try again. メールアドレスã®èªè¨¼ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„'); return; } }); diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index c883c96ba2d1d903896066f7b587e8ac6a8e7544..f834561456e17d6777527a2bc6d7d12456e784d9 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -205,7 +205,6 @@ import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; import * as ep___i_favorites from './endpoints/i/favorites.js'; import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js'; import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js'; -import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js'; import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; import * as ep___i_importFollowing from './endpoints/i/import-following.js'; import * as ep___i_importMuting from './endpoints/i/import-muting.js'; @@ -258,7 +257,6 @@ import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; -import * as ep___notes_update from './endpoints/notes/update.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; @@ -327,6 +325,7 @@ import * as ep___users_followers from './endpoints/users/followers.js'; import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; +import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js'; import * as ep___users_lists_create from './endpoints/users/lists/create.js'; import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; import * as ep___users_lists_list from './endpoints/users/lists/list.js'; @@ -336,7 +335,9 @@ import * as ep___users_lists_show from './endpoints/users/lists/show.js'; import * as ep___users_lists_update from './endpoints/users/lists/update.js'; import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; -import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; +import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js'; +import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js'; +import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; import * as ep___users_flashs from './endpoints/users/flashs.js'; @@ -554,7 +555,6 @@ const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: const $i_favorites: Provider = { provide: 'ep:i/favorites', useClass: ep___i_favorites.default }; const $i_gallery_likes: Provider = { provide: 'ep:i/gallery/likes', useClass: ep___i_gallery_likes.default }; const $i_gallery_posts: Provider = { provide: 'ep:i/gallery/posts', useClass: ep___i_gallery_posts.default }; -const $i_getWordMutedNotesCount: Provider = { provide: 'ep:i/get-word-muted-notes-count', useClass: ep___i_getWordMutedNotesCount.default }; const $i_importBlocking: Provider = { provide: 'ep:i/import-blocking', useClass: ep___i_importBlocking.default }; const $i_importFollowing: Provider = { provide: 'ep:i/import-following', useClass: ep___i_importFollowing.default }; const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep___i_importMuting.default }; @@ -607,7 +607,6 @@ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default }; const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default }; const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default }; -const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default }; const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default }; const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; @@ -676,6 +675,7 @@ const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default }; const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default }; const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default }; +const $users_featuredNotes: Provider = { provide: 'ep:users/featured-notes', useClass: ep___users_featuredNotes.default }; const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default }; const $users_lists_delete: Provider = { provide: 'ep:users/lists/delete', useClass: ep___users_lists_delete.default }; const $users_lists_list: Provider = { provide: 'ep:users/lists/list', useClass: ep___users_lists_list.default }; @@ -685,7 +685,9 @@ const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default }; const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default }; const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default }; -const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default }; +const $users_lists_createFromPublic: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_createFromPublic.default }; +const $users_lists_updateMembership: Provider = { provide: 'ep:users/lists/update-membership', useClass: ep___users_lists_updateMembership.default }; +const $users_lists_getMemberships: Provider = { provide: 'ep:users/lists/get-memberships', useClass: ep___users_lists_getMemberships.default }; const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default }; const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default }; const $users_flashs: Provider = { provide: 'ep:users/flashs', useClass: ep___users_flashs.default }; @@ -907,7 +909,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_favorites, $i_gallery_likes, $i_gallery_posts, - $i_getWordMutedNotesCount, $i_importBlocking, $i_importFollowing, $i_importMuting, @@ -960,7 +961,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_conversation, $notes_create, $notes_delete, - $notes_update, $notes_favorites_create, $notes_favorites_delete, $notes_featured, @@ -1029,6 +1029,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_following, $users_gallery_posts, $users_getFrequentlyRepliedUsers, + $users_featuredNotes, $users_lists_create, $users_lists_delete, $users_lists_list, @@ -1038,7 +1039,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_update, $users_lists_favorite, $users_lists_unfavorite, - $users_lists_create_from_public, + $users_lists_createFromPublic, + $users_lists_updateMembership, + $users_lists_getMemberships, $users_notes, $users_pages, $users_flashs, @@ -1254,7 +1257,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_favorites, $i_gallery_likes, $i_gallery_posts, - $i_getWordMutedNotesCount, $i_importBlocking, $i_importFollowing, $i_importMuting, @@ -1307,7 +1309,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_conversation, $notes_create, $notes_delete, - $notes_update, $notes_favorites_create, $notes_favorites_delete, $notes_featured, @@ -1373,6 +1374,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_following, $users_gallery_posts, $users_getFrequentlyRepliedUsers, + $users_featuredNotes, $users_lists_create, $users_lists_delete, $users_lists_list, @@ -1382,7 +1384,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_update, $users_lists_favorite, $users_lists_unfavorite, - $users_lists_create_from_public, + $users_lists_createFromPublic, + $users_lists_updateMembership, + $users_lists_getMemberships, $users_notes, $users_pages, $users_flashs, diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index 9acaa688c508c13a093870112a1e6f2c0b262dc1..badcec1b33f89c082286e662cc6473c443e84d90 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -14,6 +14,7 @@ import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; import { MiLocalUser } from '@/models/User.js'; +import { UserService } from '@/core/UserService.js'; import { AuthenticateService, AuthenticationError } from './AuthenticateService.js'; import MainStreamConnection from './stream/Connection.js'; import { ChannelsService } from './stream/ChannelsService.js'; @@ -37,6 +38,7 @@ export class StreamingApiServerService { private authenticateService: AuthenticateService, private channelsService: ChannelsService, private notificationService: NotificationService, + private usersService: UserService, ) { } @@ -130,14 +132,10 @@ export class StreamingApiServerService { this.#connections.set(connection, Date.now()); const userUpdateIntervalId = user ? setInterval(() => { - this.usersRepository.update(user.id, { - lastActiveDate: new Date(), - }); + this.usersService.updateLastActiveDate(user); }, 1000 * 60 * 5) : null; if (user) { - this.usersRepository.update(user.id, { - lastActiveDate: new Date(), - }); + this.usersService.updateLastActiveDate(user); } connection.once('close', () => { diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index b40d654f9c8b5e55f3e148b7883d272a05f86529..d12a035afac2f80725e4447c257424c1adb3976e 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -205,7 +205,6 @@ import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; import * as ep___i_favorites from './endpoints/i/favorites.js'; import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js'; import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js'; -import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js'; import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; import * as ep___i_importFollowing from './endpoints/i/import-following.js'; import * as ep___i_importMuting from './endpoints/i/import-muting.js'; @@ -258,7 +257,6 @@ import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; -import * as ep___notes_update from './endpoints/notes/update.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; @@ -327,6 +325,7 @@ import * as ep___users_followers from './endpoints/users/followers.js'; import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; +import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js'; import * as ep___users_lists_create from './endpoints/users/lists/create.js'; import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; import * as ep___users_lists_list from './endpoints/users/lists/list.js'; @@ -335,8 +334,10 @@ import * as ep___users_lists_push from './endpoints/users/lists/push.js'; import * as ep___users_lists_show from './endpoints/users/lists/show.js'; import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; -import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; +import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js'; import * as ep___users_lists_update from './endpoints/users/lists/update.js'; +import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js'; +import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; import * as ep___users_flashs from './endpoints/users/flashs.js'; @@ -552,7 +553,6 @@ const eps = [ ['i/favorites', ep___i_favorites], ['i/gallery/likes', ep___i_gallery_likes], ['i/gallery/posts', ep___i_gallery_posts], - ['i/get-word-muted-notes-count', ep___i_getWordMutedNotesCount], ['i/import-blocking', ep___i_importBlocking], ['i/import-following', ep___i_importFollowing], ['i/import-muting', ep___i_importMuting], @@ -605,7 +605,6 @@ const eps = [ ['notes/conversation', ep___notes_conversation], ['notes/create', ep___notes_create], ['notes/delete', ep___notes_delete], - ['notes/update', ep___notes_update], ['notes/favorites/create', ep___notes_favorites_create], ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], @@ -674,6 +673,7 @@ const eps = [ ['users/following', ep___users_following], ['users/gallery/posts', ep___users_gallery_posts], ['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers], + ['users/featured-notes', ep___users_featuredNotes], ['users/lists/create', ep___users_lists_create], ['users/lists/delete', ep___users_lists_delete], ['users/lists/list', ep___users_lists_list], @@ -683,7 +683,9 @@ const eps = [ ['users/lists/favorite', ep___users_lists_favorite], ['users/lists/unfavorite', ep___users_lists_unfavorite], ['users/lists/update', ep___users_lists_update], - ['users/lists/create-from-public', ep___users_lists_create_from_public], + ['users/lists/create-from-public', ep___users_lists_createFromPublic], + ['users/lists/update-membership', ep___users_lists_updateMembership], + ['users/lists/get-memberships', ep___users_lists_getMemberships], ['users/notes', ep___users_notes], ['users/pages', ep___users_pages], ['users/flashs', ep___users_flashs], diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 24d3a8a9439642934e0aa4b477c61bc5a4de250c..faab8ee608da61e9baa0ad8cb5dd5382d88dcfdd 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -23,6 +23,11 @@ export const meta = { code: 'NO_SUCH_FILE', id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', }, + duplicateName: { + message: 'Duplicate name.', + code: 'DUPLICATE_NAME', + id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', + }, }, } as const; @@ -64,6 +69,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- super(meta, paramDef, async (ps, me) => { const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); + const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); + if (isDuplicate) throw new ApiError(meta.errors.duplicateName); const emoji = await this.customEmojiService.add({ driveFile, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 2d69857408efe50bb79a6153edc5b73859c31f5e..04226d8953895d81e3ab1aa87293af79105be907 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -74,6 +74,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); } + const emoji = await this.customEmojiService.getEmojiById(ps.id); + if (emoji != null) { + if (ps.name !== emoji.name) { + const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); + if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists); + } + } else { + throw new ApiError(meta.errors.noSuchEmoji); + } await this.customEmojiService.update(ps.id, { driveFile, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index c3ba07cdd0862f7b51e74875067617f503d74b4d..5a74456ab08d0c3dde653adf0a2d259a1430b157 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -105,40 +105,32 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - userStarForReactionFallback: { - type: 'boolean', - optional: true, nullable: false, - }, pinnedUsers: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, hiddenTags: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, blockedHosts: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, sensitiveWords: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, preservedUsernames: { @@ -146,129 +138,124 @@ export const meta = { optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, hcaptchaSecretKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, recaptchaSecretKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, turnstileSecretKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, sensitiveMediaDetection: { type: 'string', - optional: true, nullable: false, + optional: false, nullable: false, }, sensitiveMediaDetectionSensitivity: { type: 'string', - optional: true, nullable: false, + optional: false, nullable: false, }, setSensitiveFlagAutomatically: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, enableSensitiveMediaDetectionForVideos: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, proxyAccountId: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, format: 'id', }, - summaryProxy: { - type: 'string', - optional: true, nullable: true, - }, email: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, smtpSecure: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, smtpHost: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, smtpPort: { type: 'number', - optional: true, nullable: true, + optional: false, nullable: true, }, smtpUser: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, smtpPass: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, swPrivateKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, useObjectStorage: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, objectStorageBaseUrl: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageBucket: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStoragePrefix: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageEndpoint: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageRegion: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStoragePort: { type: 'number', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageAccessKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageSecretKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageUseSSL: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, objectStorageUseProxy: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, objectStorageSetPublicRead: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, enableIpLogging: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, enableActiveEmailValidation: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, enableChartsForRemoteUser: { type: 'boolean', @@ -288,12 +275,32 @@ export const meta = { }, manifestJsonOverride: { type: 'string', - optional: true, nullable: false, + optional: false, nullable: false, }, policies: { type: 'object', optional: false, nullable: false, }, + perLocalUserUserTimelineCacheMax: { + type: 'number', + optional: false, nullable: false, + }, + perRemoteUserUserTimelineCacheMax: { + type: 'number', + optional: false, nullable: false, + }, + perUserHomeTimelineCacheMax: { + type: 'number', + optional: false, nullable: false, + }, + perUserListTimelineCacheMax: { + type: 'number', + optional: false, nullable: false, + }, + notesPerOneAd: { + type: 'number', + optional: false, nullable: false, + }, }, }, } as const; @@ -313,7 +320,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private metaService: MetaService, ) { - super(meta, paramDef, async (ps, me) => { + super(meta, paramDef, async () => { const instance = await this.metaService.fetch(true); return { @@ -328,6 +335,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- tosUrl: instance.termsOfServiceUrl, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, + impressumUrl: instance.impressumUrl, + privacyPolicyUrl: instance.privacyPolicyUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, @@ -399,6 +408,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- enableIdenticonGeneration: instance.enableIdenticonGeneration, policies: { ...DEFAULT_POLICIES, ...instance.policies }, manifestJsonOverride: instance.manifestJsonOverride, + perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax, + perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax, + perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, + perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, + notesPerOneAd: instance.notesPerOneAd, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index b1772be7777eb522a0c8d3dd8a3a8449603c0848..ef5627bc9a712fc81c43d7b1059f8f617b8fb86d 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -61,9 +61,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) .andWhere('assign.roleId = :roleId', { roleId: role.id }) - .andWhere(new Brackets(qb => { qb - .where('assign.expiresAt IS NULL') - .orWhere('assign.expiresAt > :now', { now: new Date() }); + .andWhere(new Brackets(qb => { + qb + .where('assign.expiresAt IS NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); })) .innerJoinAndSelect('assign.user', 'user'); diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 345459753283cf690b23df2e8e627d120bda88ea..0731413d0535ad89b5d84f30d7d8197ea6a6eb77 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -85,6 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- isModerator: isModerator, isSilenced: isSilenced, isSuspended: user.isSuspended, + isHibernated: user.isHibernated, lastActiveDate: user.lastActiveDate, moderationNote: profile.moderationNote ?? '', signins, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index ea6ebdd1fe1ab76c9b0b23a7a9b32b2de94ccde9..7db25e659f95d45ec80b168bc34aa1a5abdeea9b 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -86,6 +86,8 @@ export const paramDef = { tosUrl: { type: 'string', nullable: true }, repositoryUrl: { type: 'string' }, feedbackUrl: { type: 'string' }, + impressumUrl: { type: 'string' }, + privacyPolicyUrl: { type: 'string' }, useObjectStorage: { type: 'boolean' }, objectStorageBaseUrl: { type: 'string', nullable: true }, objectStorageBucket: { type: 'string', nullable: true }, @@ -108,6 +110,11 @@ export const paramDef = { serverRules: { type: 'array', items: { type: 'string' } }, preservedUsernames: { type: 'array', items: { type: 'string' } }, manifestJsonOverride: { type: 'string' }, + perLocalUserUserTimelineCacheMax: { type: 'integer' }, + perRemoteUserUserTimelineCacheMax: { type: 'integer' }, + perUserHomeTimelineCacheMax: { type: 'integer' }, + perUserListTimelineCacheMax: { type: 'integer' }, + notesPerOneAd: { type: 'integer' }, }, required: [], } as const; @@ -341,6 +348,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.feedbackUrl = ps.feedbackUrl; } + if (ps.impressumUrl !== undefined) { + set.impressumUrl = ps.impressumUrl; + } + + if (ps.privacyPolicyUrl !== undefined) { + set.privacyPolicyUrl = ps.privacyPolicyUrl; + } + if (ps.useObjectStorage !== undefined) { set.useObjectStorage = ps.useObjectStorage; } @@ -441,6 +456,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.manifestJsonOverride = ps.manifestJsonOverride; } + if (ps.perLocalUserUserTimelineCacheMax !== undefined) { + set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax; + } + + if (ps.perRemoteUserUserTimelineCacheMax !== undefined) { + set.perRemoteUserUserTimelineCacheMax = ps.perRemoteUserUserTimelineCacheMax; + } + + if (ps.perUserHomeTimelineCacheMax !== undefined) { + set.perUserHomeTimelineCacheMax = ps.perUserHomeTimelineCacheMax; + } + + if (ps.perUserListTimelineCacheMax !== undefined) { + set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax; + } + + if (ps.notesPerOneAd !== undefined) { + set.notesPerOneAd = ps.notesPerOneAd; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index eaae7bff62f1170e8f352ba878bd1180dcadc676..6d69971e30e78701980c69286f08c2f1f6b7d8ca 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -12,6 +12,7 @@ import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -56,8 +57,8 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -69,8 +70,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, private noteReadService: NoteReadService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const antenna = await this.antennasRepository.findOneBy({ id: ps.antennaId, userId: me.id, @@ -85,19 +90,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- lastUsedAt: new Date(), }); - const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdã«æŒ‡å®šã—ãŸã‚‚ã®ã‚‚å«ã¾ã‚Œã‚‹ãŸã‚+1 - const noteIdsRes = await this.redisClient.xrevrange( - `antennaTimeline:${antenna.id}`, - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', - ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', - 'COUNT', limit); - - if (noteIdsRes.length === 0) { - return []; - } - - const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); - + let noteIds = await this.redisTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId); + noteIds = noteIds.slice(0, ps.limit); if (noteIds.length === 0) { return []; } @@ -115,7 +109,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.queryService.generateBlockedUserQuery(query, me); const notes = await query.getMany(); - notes.sort((a, b) => a.id > b.id ? -1 : 1); + if (sinceId != null && untilId == null) { + notes.sort((a, b) => a.id < b.id ? -1 : 1); + } else { + notes.sort((a, b) => a.id > b.id ? -1 : 1); + } if (notes.length > 0) { this.noteReadService.read(me.id, notes); diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts index 65df45706b60d0419b0ce33a3686fddf701da2e3..9c78a948443f00e5a52b29c93fc226877cae5b1b 100644 --- a/packages/backend/src/server/api/endpoints/channels/search.ts +++ b/packages/backend/src/server/api/endpoints/channels/search.ts @@ -55,9 +55,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.query !== '') { 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) }%` }); + 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) }%` }); diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 026b6495372be586db9073c6435bcbc94616c091..2dfcf659d72b5a3af9d3e19323c23f2ff3a4cdb8 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -12,6 +12,9 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { CacheService } from '@/core/CacheService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -54,8 +57,8 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -66,9 +69,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, + private redisTimelineService: RedisTimelineService, + private cacheService: CacheService, private activeUsersChart: ActiveUsersChart, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const isRangeSpecified = untilId != null && sinceId != null; + const channel = await this.channelsRepository.findOneBy({ id: ps.channelId, }); @@ -77,70 +86,66 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.noSuchChannel); } - let timeline: MiNote[] = []; + if (me) this.activeUsersChart.read(me); - const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdã«æŒ‡å®šã—ãŸã‚‚ã®ã‚‚å«ã¾ã‚Œã‚‹ãŸã‚+1 - let noteIdsRes: [string, string[]][] = []; + if (isRangeSpecified || sinceId == null) { + const [ + userIdsWhoMeMuting, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + ]) : [new Set<string>()]; - if (!ps.sinceId && !ps.sinceDate) { - noteIdsRes = await this.redisClient.xrevrange( - `channelTimeline:${channel.id}`, - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', - '-', - 'COUNT', limit); - } + let noteIds = await this.redisTimelineService.get(`channelTimeline:${channel.id}`, untilId, sinceId); + noteIds = noteIds.slice(0, ps.limit); - // redis ã‹ã‚‰å–å¾—ã—ã¦ã„ãªã„ã¨ã・å–å¾—æ•°ãŒè¶³ã‚Šãªã„ã¨ã - if (noteIdsRes.length < limit) { - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.channelId = :channelId', { channelId: channel.id }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - if (me) { - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateMutedNoteQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); - } - //#endregion + if (noteIds.length > 0) { + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); - timeline = await query.limit(ps.limit).getMany(); - } else { - const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); + let timeline = await query.getMany(); - if (noteIds.length === 0) { - return []; - } + timeline = timeline.filter(note => { + if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - //#region Construct query - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - if (me) { - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateMutedNoteQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); + return true; + }); + + // TODO: フィルタã§ä»¶æ•°ãŒæ¸›ã£ãŸå ´åˆã®åŸ‹ã‚åˆã‚ã›å‡¦ç† + + timeline.sort((a, b) => a.id > b.id ? -1 : 1); + + if (timeline.length > 0) { + return await this.noteEntityService.packMany(timeline, me); + } } - //#endregion + } - timeline = await query.getMany(); - timeline.sort((a, b) => a.id > b.id ? -1 : 1); + //#region fallback to database + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.channelId = :channelId', { channelId: channel.id }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); } + //#endregion - if (me) this.activeUsersChart.read(me); + const timeline = await query.limit(ps.limit).getMany(); return await this.noteEntityService.packMany(timeline, me); + //#endregion }); } } diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index 779231a856f1fb12469130e1a610d494e1ab82dc..14a13b09c926714b3d6978c4c983ec890a3eed61 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -6,6 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { NotesRepository, DriveFilesRepository } from '@/models/_.js'; +import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -41,6 +42,9 @@ export const meta = { export const paramDef = { type: 'object', properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, fileId: { type: 'string', format: 'misskey:id' }, }, required: ['fileId'], @@ -56,6 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, + private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { // Fetch file @@ -68,9 +73,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.noSuchFile); } - const notes = await this.notesRepository.createQueryBuilder('note') - .where(':file = ANY(note.fileIds)', { file: file.id }) - .getMany(); + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId); + query.andWhere(':file = ANY(note.fileIds)', { file: file.id }); + + const notes = await query.limit(ps.limit).getMany(); return await this.noteEntityService.packMany(notes, me, { detail: true, diff --git a/packages/backend/src/server/api/endpoints/following/update.ts b/packages/backend/src/server/api/endpoints/following/update.ts index 25f393e5174504d521437f8df94629934f70b343..db17d151dfdd500fd639b11560124a54646eaa9d 100644 --- a/packages/backend/src/server/api/endpoints/following/update.ts +++ b/packages/backend/src/server/api/endpoints/following/update.ts @@ -57,8 +57,9 @@ export const paramDef = { properties: { userId: { type: 'string', format: 'misskey:id' }, notify: { type: 'string', enum: ['normal', 'none'] }, + withReplies: { type: 'boolean' }, }, - required: ['userId', 'notify'], + required: ['userId'], } as const; @Injectable() @@ -98,7 +99,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- await this.followingsRepository.update({ id: exist.id, }, { - notify: ps.notify === 'none' ? null : ps.notify, + notify: ps.notify != null ? (ps.notify === 'none' ? null : ps.notify) : undefined, + withReplies: ps.withReplies != null ? ps.withReplies : undefined, }); return await this.userEntityService.pack(follower.id, me); diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index 75d4fe3819b387de8b5c4719eef74c907c6ab2c3..8f382eb96b723e130c6194f506be7fdd3bb67d4b 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -3,29 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NotesRepository } from '@/models/_.js'; -import type { MiNote } from '@/models/Note.js'; -import { safeForSql } from '@/misc/safe-for-sql.js'; -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; - -/* -トレンドã«è¼‰ã‚‹ãŸã‚ã«ã¯ã€Œã€Žç›´è¿‘a分間ã®ãƒ¦ãƒ‹ãƒ¼ã‚¯æŠ•ç¨¿æ•°ãŒä»Šã‹ã‚‰a分å‰ï½žä»Šã‹ã‚‰b分å‰ã®é–“ã®ãƒ¦ãƒ‹ãƒ¼ã‚¯æŠ•ç¨¿æ•°ã®nå€ä»¥ä¸Šã€ã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ä¸Šä½5ä½ä»¥å†…ã«å…¥ã‚‹ã€ã“ã¨ãŒå¿…è¦ -ユニーク投稿数ã¨ã¯ãã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã¨æŠ•ç¨¿ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒšã‚¢ã®ã‚«ã‚¦ãƒ³ãƒˆã§ã€ä¾‹ãˆã°åŒã˜ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒè¤‡æ•°å›žåŒã˜ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’投稿ã—ã¦ã‚‚ãã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ãƒ¦ãƒ‹ãƒ¼ã‚¯æŠ•ç¨¿æ•°ã¯1ã¨ã‚«ã‚¦ãƒ³ãƒˆã•ã‚Œã‚‹ - -..ãŒç†æƒ³ã ã‘ã©PostgreSQLã§ã©ã†ã™ã‚‹ã®ã‹åˆ†ã‹ã‚‰ãªã„ã®ã§å˜ã«ã€Œç›´è¿‘Aã®å†…ã«æŠ•ç¨¿ã•ã‚ŒãŸãƒ¦ãƒ‹ãƒ¼ã‚¯æŠ•ç¨¿æ•°ãŒå¤šã„ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€ã§å¦¥å”ã™ã‚‹ -*/ - -const rangeA = 1000 * 60 * 60; // 60分 -//const rangeB = 1000 * 60 * 120; // 2時間 -//const coefficient = 1.25; // 「nå€ã€ã®éƒ¨åˆ† -//const requiredUsers = 3; // 最低何人ãŒãã®ã‚¿ã‚°ã‚’投稿ã—ã¦ã„ã‚‹å¿…è¦ãŒã‚ã‚‹ã‹ - -const max = 5; +import { FeaturedService } from '@/core/FeaturedService.js'; +import { HashtagService } from '@/core/HashtagService.js'; export const meta = { tags: ['hashtags'], @@ -71,98 +53,18 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - private metaService: MetaService, + private featuredService: FeaturedService, + private hashtagService: HashtagService, ) { super(meta, paramDef, async () => { - const instance = await this.metaService.fetch(true); - const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); - - const now = new Date(); // 5分å˜ä½ã§ä¸¸ã‚ãŸç¾åœ¨æ—¥æ™‚ - now.setMinutes(Math.round(now.getMinutes() / 5) * 5, 0, 0); - - const tagNotes = await this.notesRepository.createQueryBuilder('note') - .where('note.createdAt > :date', { date: new Date(now.getTime() - rangeA) }) - .andWhere(new Brackets(qb => { qb - .where('note.visibility = \'public\'') - .orWhere('note.visibility = \'home\''); - })) - .andWhere('note.tags != \'{}\'') - .select(['note.tags', 'note.userId']) - .cache(60000) // 1 min - .getMany(); - - if (tagNotes.length === 0) { - return []; - } - - const tags: { - name: string; - users: MiNote['userId'][]; - }[] = []; - - for (const note of tagNotes) { - for (const tag of note.tags) { - if (hiddenTags.includes(tag)) continue; - - const x = tags.find(x => x.name === tag); - if (x) { - if (!x.users.includes(note.userId)) { - x.users.push(note.userId); - } - } else { - tags.push({ - name: tag, - users: [note.userId], - }); - } - } - } - - // ã‚¿ã‚°ã‚’äººæ°—é †ã«ä¸¦ã¹æ›¿ãˆ - const hots = tags - .sort((a, b) => b.users.length - a.users.length) - .map(tag => tag.name) - .slice(0, max); - - //#region 2(ã¾ãŸã¯3)ã§è©±é¡Œã¨åˆ¤å®šã•ã‚ŒãŸã‚¿ã‚°ãã‚Œãžã‚Œã«ã¤ã„ã¦éŽåŽ»ã®æŠ•ç¨¿æ•°ã‚°ãƒ©ãƒ•ã‚’å–å¾—ã™ã‚‹ - const countPromises: Promise<number[]>[] = []; - - const range = 20; - - // 10分 - const interval = 1000 * 60 * 10; - - for (let i = 0; i < range; i++) { - countPromises.push(Promise.all(hots.map(tag => this.notesRepository.createQueryBuilder('note') - .select('count(distinct note.userId)') - .where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`) - .andWhere('note.createdAt < :lt', { lt: new Date(now.getTime() - (interval * i)) }) - .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - (interval * (i + 1))) }) - .cache(60000) // 1 min - .getRawOne() - .then(x => parseInt(x.count, 10)), - ))); - } - - const countsLog = await Promise.all(countPromises); - //#endregion + const ranking = await this.featuredService.getHashtagsRanking(10); - const totalCounts = await Promise.all(hots.map(tag => this.notesRepository.createQueryBuilder('note') - .select('count(distinct note.userId)') - .where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`) - .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - rangeA) }) - .cache(60000 * 60) // 60 min - .getRawOne() - .then(x => parseInt(x.count, 10)), - )); + const charts = ranking.length === 0 ? {} : await this.hashtagService.getCharts(ranking, 20); - const stats = hots.map((tag, i) => ({ + const stats = ranking.map((tag, i) => ({ tag, - chart: countsLog.map(counts => counts[i]), - usersCount: totalCounts[i], + chart: charts[tag], + usersCount: Math.max(...charts[tag]), })); return stats; diff --git a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts deleted file mode 100644 index d62bfbb3edb37a8c832caf8046851dbb355101a9..0000000000000000000000000000000000000000 --- a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MutedNotesRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['account'], - - requireCredential: true, - - kind: 'read:account', - - res: { - type: 'object', - optional: false, nullable: false, - properties: { - count: { - type: 'number', - optional: false, nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.mutedNotesRepository) - private mutedNotesRepository: MutedNotesRepository, - ) { - super(meta, paramDef, async (ps, me) => { - return { - count: await this.mutedNotesRepository.countBy({ - userId: me.id, - reason: 'word', - }), - }; - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index fa6486ed18341ef6cf8fb96936eeee50f2cab94a..2727e4f093c1b3c6c5c6ca1d5d5b7a09c35f6589 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -181,6 +181,11 @@ export const meta = { }, }, }, + notesPerOneAd: { + type: 'number', + optional: false, nullable: false, + default: 0, + }, requireSetup: { type: 'boolean', optional: false, nullable: false, @@ -214,11 +219,11 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - localTimeLine: { + localTimeline: { type: 'boolean', optional: false, nullable: false, }, - globalTimeLine: { + globalTimeline: { type: 'boolean', optional: false, nullable: false, }, @@ -299,6 +304,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- tosUrl: instance.termsOfServiceUrl, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, + impressumUrl: instance.impressumUrl, + privacyPolicyUrl: instance.privacyPolicyUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, @@ -329,6 +336,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- imageUrl: ad.imageUrl, dayOfWeek: ad.dayOfWeek, })), + notesPerOneAd: instance.notesPerOneAd, enableEmail: instance.enableEmail, enableServiceWorker: instance.enableServiceWorker, diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 1a82a4b5d7a7f602cd41a1e41b74b5444212b64b..1e569d9806d9719465e722c40b1774d8c32163f3 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -49,16 +49,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(new Brackets(qb => { qb - .where('note.replyId = :noteId', { noteId: ps.noteId }) - .orWhere(new Brackets(qb => { qb - .where('note.renoteId = :noteId', { noteId: ps.noteId }) - .andWhere(new Brackets(qb => { qb - .where('note.text IS NOT NULL') - .orWhere('note.fileIds != \'{}\'') - .orWhere('note.hasPoll = TRUE'); + .andWhere(new Brackets(qb => { + qb + .where('note.replyId = :noteId', { noteId: ps.noteId }) + .orWhere(new Brackets(qb => { + qb + .where('note.renoteId = :noteId', { noteId: ps.noteId }) + .andWhere(new Brackets(qb => { + qb + .where('note.text IS NOT NULL') + .orWhere('note.fileIds != \'{}\'') + .orWhere('note.hasPoll = TRUE'); + })); })); - })); })) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 37a0525e25dd134813a49d9d07f3c1f6ea06f366..3ae4ac044a82d70ff9153e6c2050919cab63e386 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -57,6 +57,12 @@ export const meta = { id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a', }, + cannotRenoteDueToVisibility: { + message: 'You can not Renote due to target visibility.', + code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY', + id: 'be9529e9-fe72-4de0-ae43-0b363c4938af', + }, + noSuchReplyTarget: { message: 'No such reply target.', code: 'NO_SUCH_REPLY_TARGET', @@ -231,6 +237,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.youHaveBeenBlocked); } } + + if (renote.visibility === 'followers' && renote.userId !== me.id) { + // 他人ã®followers noteã¯reject + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } else if (renote.visibility === 'specified') { + // specified / direct noteã¯reject + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } } let reply: MiNote | null = null; diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index 5283b0e0bc8e6c78d1ff5685b341bd8fc2b55fc4..c456874309c5bce9113c1807158c05af297f76ac 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -6,9 +6,9 @@ import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; export const meta = { tags: ['notes'], @@ -32,7 +32,7 @@ export const paramDef = { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - offset: { type: 'integer', default: 0 }, + untilId: { type: 'string', format: 'misskey:id' }, channelId: { type: 'string', nullable: true, format: 'misskey:id' }, }, required: [], @@ -40,41 +40,53 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + private globalNotesRankingCache: string[] = []; + private globalNotesRankingCacheLastFetchedAt = 0; + constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, - private queryService: QueryService, + private featuredService: FeaturedService, ) { super(meta, paramDef, async (ps, me) => { - const day = 1000 * 60 * 60 * 24 * 3; // 3æ—¥å‰ã¾ã§ + let noteIds: string[]; + if (ps.channelId) { + noteIds = await this.featuredService.getInChannelNotesRanking(ps.channelId, 50); + } else { + if (this.globalNotesRankingCacheLastFetchedAt !== 0 && (Date.now() - this.globalNotesRankingCacheLastFetchedAt < 1000 * 60 * 30)) { + noteIds = this.globalNotesRankingCache; + } else { + noteIds = await this.featuredService.getGlobalNotesRanking(100); + this.globalNotesRankingCache = noteIds; + this.globalNotesRankingCacheLastFetchedAt = Date.now(); + } + } + + if (noteIds.length === 0) { + return []; + } + + noteIds.sort((a, b) => a > b ? -1 : 1); + if (ps.untilId) { + noteIds = noteIds.filter(id => id < ps.untilId!); + } + noteIds = noteIds.slice(0, ps.limit); const query = this.notesRepository.createQueryBuilder('note') - .addSelect('note.score') - .where('note.userHost IS NULL') - .andWhere('note.score > 0') - .andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) }) - .andWhere('note.visibility = \'public\'') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - if (ps.channelId) query.andWhere('note.channelId = :channelId', { channelId: ps.channelId }); - - if (me) this.queryService.generateMutedUserQuery(query, me); - if (me) this.queryService.generateBlockedUserQuery(query, me); - - let notes = await query - .orderBy('note.score', 'DESC') - .limit(100) - .getMany(); + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); - notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + const notes = await query.getMany(); + notes.sort((a, b) => a.id > b.id ? -1 : 1); - notes = notes.slice(ps.offset, ps.offset + ps.limit); + // TODO: ミュートç‰è€ƒæ…® return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 8784e86153773a48b571851de74ecb272f05b0f9..be7557c2137adde0cba4f0a0fc61f109987fb085 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -40,7 +40,6 @@ export const paramDef = { type: 'object', properties: { withFiles: { type: 'boolean', default: false }, - withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, @@ -79,10 +78,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - this.queryService.generateRepliesQuery(query, ps.withReplies, me); if (me) { this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateMutedNoteQuery(query, me); this.queryService.generateBlockedUserQuery(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me); } @@ -90,16 +87,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); } - - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } //#endregion const timeline = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 9bde5dee21a6badc46bc3bec731ef569703336c6..1b77285d47ccbf95c6aa95df2d6c0cecf58951a0 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -5,14 +5,17 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, FollowingsRepository } from '@/models/_.js'; +import * as Redis from 'ioredis'; +import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { CacheService } from '@/core/CacheService.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -51,7 +54,6 @@ export const paramDef = { includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, - withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, }, required: [], @@ -60,97 +62,81 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - private noteEntityService: NoteEntityService, - private queryService: QueryService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, private idService: IdService, + private cacheService: CacheService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const policies = await this.roleService.getUserPolicies(me.id); if (!policies.ltlAvailable) { throw new ApiError(meta.errors.stlDisabled); } - //#region Construct query - const followingQuery = this.followingsRepository.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); - - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10æ—¥å‰ã¾ã§ - .andWhere(new Brackets(qb => { - qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id }) - .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); - })) + const [ + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + ] = await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.renoteMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]); + + const [htlNoteIds, ltlNoteIds] = await this.redisTimelineService.getMulti([ + ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, + ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline', + ], untilId, sinceId); + + let noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); + noteIds.sort((a, b) => a > b ? -1 : 1); + noteIds = noteIds.slice(0, ps.limit); + + if (noteIds.length === 0) { + return []; + } + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser') - .setParameters(followingQuery.getParameters()); - - this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, ps.withReplies, me); - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateMutedNoteQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); - this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } + .leftJoinAndSelect('note.channel', 'channel'); + + let timeline = await query.getMany(); + + timeline = timeline.filter(note => { + if (note.userId === me.id) { + return true; + } + if (isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (isUserRelated(note, userIdsWhoMeMuting)) return false; + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; + if (ps.withRenotes === false) return false; + } + } + + return true; + }); - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } - //#endregion + // TODO: フィルタã—ãŸçµæžœä»¶æ•°ãŒè¶³ã‚Šãªã‹ã£ãŸå ´åˆã®å¯¾å¿œ - const timeline = await query.limit(ps.limit).getMany(); + timeline.sort((a, b) => a.id > b.id ? -1 : 1); process.nextTick(() => { this.activeUsersChart.read(me); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 0fefddc51b02b972a9c469078958508d875af8d8..2357f32d5e9bb6f02aa624f581d6630c231aafa2 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -5,14 +5,17 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import * as Redis from 'ioredis'; +import type { MiNote, NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; +import { CacheService } from '@/core/CacheService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -41,11 +44,7 @@ export const paramDef = { type: 'object', properties: { withFiles: { type: 'boolean', default: false }, - withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, - fileType: { type: 'array', items: { - type: 'string', - } }, excludeNsfw: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, @@ -59,71 +58,75 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, - private queryService: QueryService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, private idService: IdService, + private cacheService: CacheService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const policies = await this.roleService.getUserPolicies(me ? me.id : null); if (!policies.ltlAvailable) { throw new ApiError(meta.errors.ltlDisabled); } - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10æ—¥å‰ã¾ã§ - .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') + const [ + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.renoteMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]) : [new Set<string>(), new Set<string>(), new Set<string>()]; + + let noteIds = await this.redisTimelineService.get(ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline', untilId, sinceId); + noteIds = noteIds.slice(0, ps.limit); + + if (noteIds.length === 0) { + return []; + } + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, ps.withReplies, me); - this.queryService.generateVisibilityQuery(query, me); - if (me) this.queryService.generateMutedUserQuery(query, me); - if (me) this.queryService.generateMutedNoteQuery(query, me); - if (me) this.queryService.generateBlockedUserQuery(query, me); - if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); - if (ps.fileType != null) { - query.andWhere('note.fileIds != \'{}\''); - query.andWhere(new Brackets(qb => { - for (const type of ps.fileType!) { - const i = ps.fileType!.indexOf(type); - qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); - } - })); + let timeline = await query.getMany(); - if (ps.excludeNsfw) { - query.andWhere('note.cw IS NULL'); - query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); + timeline = timeline.filter(note => { + if (me && (note.userId === me.id)) { + return true; + } + if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; + if (ps.withRenotes === false) return false; + } } - } - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } - //#endregion + return true; + }); + + // TODO: フィルタã—ãŸçµæžœä»¶æ•°ãŒè¶³ã‚Šãªã‹ã£ãŸå ´åˆã®å¯¾å¿œ - const timeline = await query.limit(ps.limit).getMany(); + timeline.sort((a, b) => a.id > b.id ? -1 : 1); process.nextTick(() => { if (me) { diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 65e7bd8cd5188879d94056d06d28eda4eef75554..6fab024d17a044bab79cf8c0c80e15dc6cab1546 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -59,9 +59,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .where('following.followerId = :followerId', { followerId: me.id }); const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(new Brackets(qb => { qb - .where(`'{"${me.id}"}' <@ note.mentions`) - .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`); + .andWhere(new Brackets(qb => { + qb + .where(`'{"${me.id}"}' <@ note.mentions`) + .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`); })) // Avoid scanning primary key index .orderBy('CONCAT(note.id)', 'DESC') diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index 29190af62a2c77c4acae6e106369bca27c0bd64c..986201e95083afeb822cf379c5fed3fce4e9b011 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -57,9 +57,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .where('poll.userHost IS NULL') .andWhere('poll.userId != :meId', { meId: me.id }) .andWhere('poll.noteVisibility = \'public\'') - .andWhere(new Brackets(qb => { qb - .where('poll.expiresAt IS NULL') - .orWhere('poll.expiresAt > :now', { now: new Date() }); + .andWhere(new Brackets(qb => { + qb + .where('poll.expiresAt IS NULL') + .orWhere('poll.expiresAt > :now', { now: new Date() }); })); //#region exclude arleady voted polls diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 0d47cc17020f7a1b85cd31f551da86a556c4d2f1..760d52c9db0e89961caa9c730aed80db834bc1f5 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -5,13 +5,17 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, FollowingsRepository } from '@/models/_.js'; +import * as Redis from 'ioredis'; +import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; +import { CacheService } from '@/core/CacheService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; export const meta = { tags: ['notes'], @@ -41,7 +45,6 @@ export const paramDef = { includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, - withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, }, required: [], @@ -50,96 +53,74 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - private noteEntityService: NoteEntityService, - private queryService: QueryService, private activeUsersChart: ActiveUsersChart, private idService: IdService, + private cacheService: CacheService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { - const followees = await this.followingsRepository.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }) - .getMany(); - - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - // パフォーマンス上ã®åˆ©ç‚¹ãŒç„¡ã•ãã†ï¼Ÿ - //.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10æ—¥å‰ã¾ã§ + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + + const [ + followings, + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + ] = await Promise.all([ + this.cacheService.userFollowingsCache.fetch(me.id), + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.renoteMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]); + + let noteIds = await this.redisTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId); + noteIds = noteIds.slice(0, ps.limit); + + if (noteIds.length === 0) { + return []; + } + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - if (followees.length > 0) { - const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; - - query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); - } else { - query.andWhere('note.userId = :meId', { meId: me.id }); - } - - this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, ps.withReplies, me); - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateMutedNoteQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); - this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + let timeline = await query.getMany(); + + timeline = timeline.filter(note => { + if (note.userId === me.id) { + return true; + } + if (isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (isUserRelated(note, userIdsWhoMeMuting)) return false; + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; + if (ps.withRenotes === false) return false; + } + } + if (note.reply && note.reply.visibility === 'followers') { + if (!Object.hasOwn(followings, note.reply.userId)) return false; + } + + return true; + }); - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } - //#endregion + // TODO: フィルタã—ãŸçµæžœä»¶æ•°ãŒè¶³ã‚Šãªã‹ã£ãŸå ´åˆã®å¯¾å¿œ - const timeline = await query.limit(ps.limit).getMany(); + timeline.sort((a, b) => a.id > b.id ? -1 : 1); process.nextTick(() => { this.activeUsersChart.read(me); diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts deleted file mode 100644 index cdf7f085e0f1fc5508a696be7c3a21a7fc6925b4..0000000000000000000000000000000000000000 --- a/packages/backend/src/server/api/endpoints/notes/update.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import ms from 'ms'; -import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, NotesRepository } from '@/models/_.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NoteDeleteService } from '@/core/NoteDeleteService.js'; -import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['notes'], - - requireCredential: true, - requireRolePolicy: 'canEditNote', - - kind: 'write:notes', - - limit: { - duration: ms('1hour'), - max: 10, - minInterval: ms('1sec'), - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - text: { - type: 'string', - minLength: 1, - maxLength: MAX_NOTE_TEXT_LENGTH, - nullable: false, - }, - cw: { type: 'string', nullable: true, maxLength: 100 }, - }, - required: ['noteId', 'text', 'cw'], -} as const; - -@Injectable() -export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - private getterService: GetterService, - private globalEventService: GlobalEventService, - ) { - super(meta, paramDef, async (ps, me) => { - const note = await this.getterService.getNote(ps.noteId).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - if (note.userId !== me.id) { - throw new ApiError(meta.errors.noSuchNote); - } - - await this.notesRepository.update({ id: note.id }, { - updatedAt: new Date(), - cw: ps.cw, - text: ps.text, - }); - - this.globalEventService.publishNoteStream(note.id, 'updated', { - cw: ps.cw, - text: ps.text, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index c20274b2baf12073e1c6f1975a0435881c4ceddf..f7ee58264e2fae3e40010b892bf55eef96484a61 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -5,12 +5,17 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/_.js'; +import * as Redis from 'ioredis'; +import type { NotesRepository, UserListsRepository, UserListMembershipsRepository, MiNote } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; +import { CacheService } from '@/core/CacheService.js'; +import { IdService } from '@/core/IdService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -49,7 +54,6 @@ export const paramDef = { includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, - withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', @@ -63,20 +67,25 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, - private noteEntityService: NoteEntityService, - private queryService: QueryService, private activeUsersChart: ActiveUsersChart, + private cacheService: CacheService, + private idService: IdService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const list = await this.userListsRepository.findOneBy({ id: ps.listId, userId: me.id, @@ -86,72 +95,53 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.noSuchList); } - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .innerJoin(this.userListJoiningsRepository.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId') + const [ + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + ] = await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.renoteMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]); + + let noteIds = await this.redisTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId); + noteIds = noteIds.slice(0, ps.limit); + + if (noteIds.length === 0) { + return []; + } + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser') - .andWhere('userListJoining.userListId = :userListId', { userListId: list.id }); - - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateMutedNoteQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); - this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (!ps.withReplies) { - query.andWhere('note.replyId IS NULL'); - } - - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } + .leftJoinAndSelect('note.channel', 'channel'); + + let timeline = await query.getMany(); + + timeline = timeline.filter(note => { + if (note.userId === me.id) { + return true; + } + if (isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (isUserRelated(note, userIdsWhoMeMuting)) return false; + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; + if (ps.withRenotes === false) return false; + } + } + + return true; + }); - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion + // TODO: フィルタã—ãŸçµæžœä»¶æ•°ãŒè¶³ã‚Šãªã‹ã£ãŸå ´åˆã®å¯¾å¿œ - const timeline = await query.limit(ps.limit).getMany(); + timeline.sort((a, b) => a.id > b.id ? -1 : 1); this.activeUsersChart.read(me); diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index 6dc35907e1f27a40a35e0336d8c30bab04ec6564..0db51abc5557c457d5d43801335b5a244b542590 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -11,6 +11,7 @@ import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -53,8 +54,8 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -65,8 +66,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const role = await this.rolesRepository.findOneBy({ id: ps.roleId, isPublic: true, @@ -78,18 +83,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (!role.isExplorable) { return []; } - const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdã«æŒ‡å®šã—ãŸã‚‚ã®ã‚‚å«ã¾ã‚Œã‚‹ãŸã‚+1 - const noteIdsRes = await this.redisClient.xrevrange( - `roleTimeline:${role.id}`, - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', - ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', - 'COUNT', limit); - - if (noteIdsRes.length === 0) { - return []; - } - const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); + let noteIds = await this.redisTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId); + noteIds = noteIds.slice(0, ps.limit); if (noteIds.length === 0) { return []; diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index 37aac908b53c4a2f5aed5916623935343611ade9..caaa3735e99c83f059520f407ac1837c31e7c3a0 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -62,9 +62,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) .andWhere('assign.roleId = :roleId', { roleId: role.id }) - .andWhere(new Brackets(qb => { qb - .where('assign.expiresAt IS NULL') - .orWhere('assign.expiresAt > :now', { now: new Date() }); + .andWhere(new Brackets(qb => { + qb + .where('assign.expiresAt IS NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); })) .innerJoinAndSelect('assign.user', 'user'); diff --git a/packages/backend/src/server/api/endpoints/users/featured-notes.ts b/packages/backend/src/server/api/endpoints/users/featured-notes.ts new file mode 100644 index 0000000000000000000000000000000000000000..dec0b7a122e854e2dfc02f426adeec3114e298ba --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/featured-notes.ts @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: false, + allowGet: true, + cacheSec: 3600, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Note', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + untilId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private featuredService: FeaturedService, + ) { + super(meta, paramDef, async (ps, me) => { + let noteIds = await this.featuredService.getPerUserNotesRanking(ps.userId, 50); + + noteIds.sort((a, b) => a > b ? -1 : 1); + if (ps.untilId) { + noteIds = noteIds.filter(id => id < ps.untilId!); + } + noteIds = noteIds.slice(0, ps.limit); + + if (noteIds.length === 0) { + return []; + } + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + const notes = await query.getMany(); + notes.sort((a, b) => a.id > b.id ? -1 : 1); + + // TODO: ミュートç‰è€ƒæ…® + + return await this.noteEntityService.packMany(notes, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts index eae55905d35f954eca084b6e372a37a3989709c9..f2f6c4303a686993db0a4e686612c4d14d3a4b16 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/_.js'; +import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import type { MiUserList } from '@/models/UserList.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -76,8 +76,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -110,7 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- name: ps.name, } as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); - const users = (await this.userListJoiningsRepository.findBy({ + const users = (await this.userListMembershipsRepository.findBy({ userListId: ps.listId, })).map(x => x.userId); @@ -132,7 +132,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } - const exist = await this.userListJoiningsRepository.exist({ + const exist = await this.userListMembershipsRepository.exist({ where: { userListId: userList.id, userId: currentUser.id, diff --git a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts new file mode 100644 index 0000000000000000000000000000000000000000..ae8b4e9b8124f53fab60293495c94e999e4e8259 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UserListsRepository, UserListFavoritesRepository, UserListMembershipsRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { QueryService } from '@/core/QueryService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['lists', 'account'], + + requireCredential: false, + + kind: 'read:account', + + errors: { + noSuchList: { + message: 'No such list.', + code: 'NO_SUCH_LIST', + id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + listId: { type: 'string', format: 'misskey:id' }, + forPublic: { type: 'boolean', default: false }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, + required: ['listId'], +} as const; + +@Injectable() // eslint-disable-next-line import/no-default-export +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, + + private userListEntityService: UserListEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the list + const userList = await this.userListsRepository.findOneBy(!ps.forPublic && me !== null ? { + id: ps.listId, + userId: me.id, + } : { + id: ps.listId, + isPublic: true, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } + + const query = this.queryService.makePaginationQuery(this.userListMembershipsRepository.createQueryBuilder('membership'), ps.sinceId, ps.untilId) + .andWhere('membership.userListId = :userListId', { userListId: userList.id }) + .innerJoinAndSelect('membership.user', 'user'); + + const memberships = await query + .limit(ps.limit) + .getMany(); + + return this.userListEntityService.packMembershipsMany(memberships); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 72a6a7380d9beee1f15c2d838f2bfadcda0d7153..c4ceec575b92f880b1f78d1df999e4e3776582aa 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; -import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/_.js'; +import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserListService } from '@/core/UserListService.js'; @@ -76,8 +76,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -115,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } - const exist = await this.userListJoiningsRepository.exist({ + const exist = await this.userListMembershipsRepository.exist({ where: { userListId: userList.id, userId: user.id, diff --git a/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts b/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts new file mode 100644 index 0000000000000000000000000000000000000000..b69465b940e84e8e70ab9e0bc96308da62ae23e5 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UserListsRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { DI } from '@/di-symbols.js'; +import { UserListService } from '@/core/UserListService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['lists', 'users'], + + requireCredential: true, + + prohibitMoved: true, + + kind: 'write:account', + + errors: { + noSuchList: { + message: 'No such list.', + code: 'NO_SUCH_LIST', + id: '7f44670e-ab16-43b8-b4c1-ccd2ee89cc02', + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '588e7f72-c744-4a61-b180-d354e912bda2', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + listId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id' }, + withReplies: { type: 'boolean' }, + }, + required: ['listId', 'userId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + private userListService: UserListService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the list + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + userId: me.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } + + // Fetch the user + const user = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + await this.userListService.updateMembership(user, userList, { + withReplies: ps.withReplies, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index e660a0bb25ab03fd07f0b21ea5f0d34adf80dc2b..dfef35986e6af426574fd3383340453d8b7bc86a 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -5,19 +5,21 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import * as Redis from 'ioredis'; +import type { MiNote, NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; +import { CacheService } from '@/core/CacheService.js'; +import { IdService } from '@/core/IdService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { QueryService } from '@/core/QueryService.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { tags: ['users', 'notes'], - description: 'Show all notes that this user created.', - res: { type: 'array', optional: false, nullable: false, @@ -43,6 +45,7 @@ export const paramDef = { userId: { type: 'string', format: 'misskey:id' }, withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, + withChannelNotes: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -50,9 +53,6 @@ export const paramDef = { untilDate: { type: 'integer' }, includeMyRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, - fileType: { type: 'array', items: { - type: 'string', - } }, excludeNsfw: { type: 'boolean', default: false }, }, required: ['userId'], @@ -61,23 +61,88 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, private queryService: QueryService, - private getterService: GetterService, + private cacheService: CacheService, + private idService: IdService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { - // Lookup user - const user = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }); + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const isRangeSpecified = untilId != null && sinceId != null; + const isSelf = me && (me.id === ps.userId); + + if (isRangeSpecified || sinceId == null) { + const [ + userIdsWhoMeMuting, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + ]) : [new Set<string>()]; + + const [noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([ + this.redisTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId), + ps.withReplies ? this.redisTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), + ps.withChannelNotes ? this.redisTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), + ]); + + let noteIds = Array.from(new Set([ + ...noteIdsRes, + ...repliesNoteIdsRes, + ...channelNoteIdsRes, + ])); + noteIds.sort((a, b) => a > b ? -1 : 1); + noteIds = noteIds.slice(0, ps.limit); + + if (noteIds.length > 0) { + const isFollowing = me && Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId); + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + let timeline = await query.getMany(); + + timeline = timeline.filter(note => { + if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false; + + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (ps.withRenotes === false) return false; + } + } + + if (note.channel?.isSensitive && !isSelf) return false; + if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; + if (note.visibility === 'followers' && !isFollowing && !isSelf) return false; + + return true; + }); + + // TODO: フィルタã§ä»¶æ•°ãŒæ¸›ã£ãŸå ´åˆã®åŸ‹ã‚åˆã‚ã›å‡¦ç† + + timeline.sort((a, b) => a.id > b.id ? -1 : 1); + + if (timeline.length > 0) { + return await this.noteEntityService.packMany(timeline, me); + } + } + } - //#region Construct query + //#region fallback to database const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.userId = :userId', { userId: user.id }) + .andWhere('note.userId = :userId', { userId: ps.userId }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') @@ -85,14 +150,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - query.andWhere(new Brackets(qb => { - qb.orWhere('note.channelId IS NULL'); - qb.orWhere('channel.isSensitive = false'); - })); + if (!ps.withChannelNotes) { + query.andWhere('note.channelId IS NULL'); + } this.queryService.generateVisibilityQuery(query, me); if (me) { - this.queryService.generateMutedUserQuery(query, me, user); + this.queryService.generateMutedUserQuery(query, me, { id: ps.userId }); this.queryService.generateBlockedUserQuery(query, me); } @@ -100,38 +164,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- query.andWhere('note.fileIds != \'{}\''); } - if (ps.fileType != null) { - query.andWhere('note.fileIds != \'{}\''); - query.andWhere(new Brackets(qb => { - for (const type of ps.fileType!) { - const i = ps.fileType!.indexOf(type); - qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); - } - })); - - if (ps.excludeNsfw) { - query.andWhere('note.cw IS NULL'); - query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); - } - } - - if (!ps.withReplies) { - query.andWhere('note.replyId IS NULL'); - } - - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } - if (ps.includeMyRenotes === false) { query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :userId', { userId: user.id }); + qb.orWhere('note.userId != :userId', { userId: ps.userId }); qb.orWhere('note.renoteId IS NULL'); qb.orWhere('note.text IS NOT NULL'); qb.orWhere('note.fileIds != \'{}\''); @@ -139,11 +174,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- })); } - //#endregion - const timeline = await query.limit(ps.limit).getMany(); return await this.noteEntityService.packMany(timeline, me); + //#endregion }); } } diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 74408cc64aab631075bbec34487134876055c252..4bf25d9fbb676a195f2d5bbd2eb90f10fbb01d1a 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -92,9 +92,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .andWhere(`user.id IN (${ followingQuery.getQuery() })`) .andWhere('user.id != :meId', { meId: me.id }) .andWhere('user.isSuspended = FALSE') - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); })); query.setParameters(followingQuery.getParameters()); diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index aff5b98779b8c92ca0c196a9584df5f122531917..32b5c123722715029ce4ffdbf821ac82f1c99212 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -64,9 +64,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (isUsername) { const usernameQuery = this.usersRepository.createQueryBuilder('user') .where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' }) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); })) .andWhere('user.isSuspended = FALSE'); @@ -91,9 +92,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' }); } })) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); })) .andWhere('user.isSuspended = FALSE'); @@ -122,9 +124,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const query = this.usersRepository.createQueryBuilder('user') .where(`user.id IN (${ profQuery.getQuery() })`) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); })) .andWhere('user.isSuspended = FALSE') .setParameters(profQuery.getParameters()); diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index a73071ea9985c922da9645fbec676fd85275d5da..f981e63871d287f37b65ece3e278b8abe4331727 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -11,7 +11,7 @@ import type { NoteReadService } from '@/core/NoteReadService.js'; import type { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; -import { MiUserProfile } from '@/models/_.js'; +import { MiFollowing, MiUserProfile } from '@/models/_.js'; import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js'; import type { ChannelsService } from './ChannelsService.js'; import type { EventEmitter } from 'events'; @@ -30,7 +30,7 @@ export default class Connection { private subscribingNotes: any = {}; private cachedNotes: Packed<'Note'>[] = []; public userProfile: MiUserProfile | null = null; - public following: Set<string> = new Set(); + public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {}; public followingChannels: Set<string> = new Set(); public userIdsWhoMeMuting: Set<string> = new Set(); public userIdsWhoBlockingMe: Set<string> = new Set(); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index fef52b68561e6a9545cda6a28aeef9810934d954..03f2dff62b7b5eb7fc8423cdd3a43de12a23b9ec 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -16,10 +16,10 @@ import Channel from '../channel.js'; class GlobalTimelineChannel extends Channel { public readonly chName = 'globalTimeline'; - public static shouldShare = true; + public static shouldShare = false; public static requireCredential = false; - private withReplies: boolean; private withRenotes: boolean; + private withFiles: boolean; constructor( private metaService: MetaService, @@ -38,8 +38,8 @@ class GlobalTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.gtlAvailable) return; - this.withReplies = params.withReplies ?? false; this.withRenotes = params.withRenotes ?? true; + this.withFiles = params.withFiles ?? false; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -47,6 +47,8 @@ class GlobalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { + if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + if (note.visibility !== 'public') return; if (note.channelId != null) return; @@ -64,7 +66,7 @@ class GlobalTimelineChannel extends Channel { } // 関係ãªã„返信ã¯é™¤å¤– - if (note.reply && !this.withReplies) { + if (note.reply && !this.following[note.userId]?.withReplies) { const reply = note.reply; // 「ãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€Œãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ãŒè¡Œã£ãŸè¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€ŒæŠ•ç¨¿è€…ã®æŠ•ç¨¿è€…自身ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã„å ´åˆ if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; @@ -82,13 +84,6 @@ class GlobalTimelineChannel extends Channel { if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã™ã¹ãNoteã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ - // TODO: å°†æ¥çš„ã«ã¯ã€å˜ã«MutedNoteテーブルã«ãƒ¬ã‚³ãƒ¼ãƒ‰ãŒã‚ã‚‹ã‹ã©ã†ã‹ã§åˆ¤å®šã—ãŸã„(以下ã®ç†ç”±ã«ã‚ˆã‚Šé›£ã—ãã†ã§ã¯ã‚ã‚‹) - // ç¾çŠ¶ã§ã¯ã€ãƒ¯ãƒ¼ãƒ‰ãƒŸãƒ¥ãƒ¼ãƒˆã«ãŠã‘ã‚‹MutedNoteレコードã®è¿½åŠ 処ç†ã¯ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã«æµã™å‡¦ç†ã¨ä¸¦åˆ—ã§è¡Œã‚れるãŸã‚〠- // レコードãŒè¿½åŠ ã•ã‚Œã‚‹Noteã§ã‚‚è¿½åŠ ã•ã‚Œã‚‹ã‚ˆã‚Šå…ˆã«ã“ã“ã®ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã®å‡¦ç†ã«åˆ°é”ã™ã‚‹ã“ã¨ãŒèµ·ã“る。 - // ãã®ãŸã‚レコードãŒå˜åœ¨ã™ã‚‹ã‹ã®ãƒã‚§ãƒƒã‚¯ã§ã¯ä¸å分ãªã®ã§ã€æ”¹ã‚ã¦checkWordMuteを呼んã§ã„ã‚‹ - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - this.connection.cacheNote(note); this.send('note', note); diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 198c68e1c2584cf7a00be088d3ec445ed3e47b43..24be59050492ae96c405278e03699b6fb8331ad1 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -14,10 +14,10 @@ import Channel from '../channel.js'; class HomeTimelineChannel extends Channel { public readonly chName = 'homeTimeline'; - public static shouldShare = true; + public static shouldShare = false; public static requireCredential = true; - private withReplies: boolean; private withRenotes: boolean; + private withFiles: boolean; constructor( private noteEntityService: NoteEntityService, @@ -31,19 +31,21 @@ class HomeTimelineChannel extends Channel { @bindThis public async init(params: any) { - this.withReplies = params.withReplies ?? false; this.withRenotes = params.withRenotes ?? true; + this.withFiles = params.withFiles ?? false; this.subscriber.on('notesStream', this.onNote); } @bindThis private async onNote(note: Packed<'Note'>) { + if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + if (note.channelId) { if (!this.followingChannels.has(note.channelId)) return; } else { // ãã®æŠ•ç¨¿ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’フォãƒãƒ¼ã—ã¦ã„ãªã‹ã£ãŸã‚‰å¼¾ã - if ((this.user!.id !== note.userId) && !this.following.has(note.userId)) return; + if ((this.user!.id !== note.userId) && !Object.hasOwn(this.following, note.userId)) return; } // Ignore notes from instances the user has muted @@ -73,7 +75,7 @@ class HomeTimelineChannel extends Channel { } // 関係ãªã„返信ã¯é™¤å¤– - if (note.reply && !this.withReplies) { + if (note.reply && !this.following[note.userId]?.withReplies) { const reply = note.reply; // 「ãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€Œãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ãŒè¡Œã£ãŸè¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€ŒæŠ•ç¨¿è€…ã®æŠ•ç¨¿è€…自身ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã„å ´åˆ if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; @@ -88,13 +90,6 @@ class HomeTimelineChannel extends Channel { if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã™ã¹ãNoteã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ - // TODO: å°†æ¥çš„ã«ã¯ã€å˜ã«MutedNoteテーブルã«ãƒ¬ã‚³ãƒ¼ãƒ‰ãŒã‚ã‚‹ã‹ã©ã†ã‹ã§åˆ¤å®šã—ãŸã„(以下ã®ç†ç”±ã«ã‚ˆã‚Šé›£ã—ãã†ã§ã¯ã‚ã‚‹) - // ç¾çŠ¶ã§ã¯ã€ãƒ¯ãƒ¼ãƒ‰ãƒŸãƒ¥ãƒ¼ãƒˆã«ãŠã‘ã‚‹MutedNoteレコードã®è¿½åŠ 処ç†ã¯ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã«æµã™å‡¦ç†ã¨ä¸¦åˆ—ã§è¡Œã‚れるãŸã‚〠- // レコードãŒè¿½åŠ ã•ã‚Œã‚‹Noteã§ã‚‚è¿½åŠ ã•ã‚Œã‚‹ã‚ˆã‚Šå…ˆã«ã“ã“ã®ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã®å‡¦ç†ã«åˆ°é”ã™ã‚‹ã“ã¨ãŒèµ·ã“る。 - // ãã®ãŸã‚レコードãŒå˜åœ¨ã™ã‚‹ã‹ã®ãƒã‚§ãƒƒã‚¯ã§ã¯ä¸å分ãªã®ã§ã€æ”¹ã‚ã¦checkWordMuteを呼んã§ã„ã‚‹ - if (await checkWordMute(note, this.user, this.userProfile!.mutedWords)) return; - this.connection.cacheNote(note); this.send('note', note); diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index cde4297478e26c62baaab6682dce70162039cd55..d5f5d54e46e01ab16bdcedb4c0657fa95faf680f 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -16,10 +16,10 @@ import Channel from '../channel.js'; class HybridTimelineChannel extends Channel { public readonly chName = 'hybridTimeline'; - public static shouldShare = true; + public static shouldShare = false; public static requireCredential = true; - private withReplies: boolean; private withRenotes: boolean; + private withFiles: boolean; constructor( private metaService: MetaService, @@ -38,8 +38,8 @@ class HybridTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; - this.withReplies = params.withReplies ?? false; this.withRenotes = params.withRenotes ?? true; + this.withFiles = params.withFiles ?? false; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -47,13 +47,15 @@ class HybridTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { + if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + // ãƒãƒ£ãƒ³ãƒãƒ«ã®æŠ•ç¨¿ã§ã¯ãªãã€è‡ªåˆ†è‡ªèº«ã®æŠ•ç¨¿ ã¾ãŸã¯ // ãƒãƒ£ãƒ³ãƒãƒ«ã®æŠ•ç¨¿ã§ã¯ãªãã€ãã®æŠ•ç¨¿ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’フォãƒãƒ¼ã—ã¦ã„ã‚‹ ã¾ãŸã¯ // ãƒãƒ£ãƒ³ãƒãƒ«ã®æŠ•ç¨¿ã§ã¯ãªãã€å…¨ä½“公開ã®ãƒãƒ¼ã‚«ãƒ«ã®æŠ•ç¨¿ ã¾ãŸã¯ // フォãƒãƒ¼ã—ã¦ã„ã‚‹ãƒãƒ£ãƒ³ãƒãƒ«ã®æŠ•ç¨¿ ã®å ´åˆã ã‘ if (!( (note.channelId == null && this.user!.id === note.userId) || - (note.channelId == null && this.following.has(note.userId)) || + (note.channelId == null && Object.hasOwn(this.following, note.userId)) || (note.channelId == null && (note.user.host == null && note.visibility === 'public')) || (note.channelId != null && this.followingChannels.has(note.channelId)) )) return; @@ -85,7 +87,7 @@ class HybridTimelineChannel extends Channel { if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return; // 関係ãªã„返信ã¯é™¤å¤– - if (note.reply && !this.withReplies) { + if (note.reply && !this.following[note.userId]?.withReplies) { const reply = note.reply; // 「ãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€Œãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ãŒè¡Œã£ãŸè¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€ŒæŠ•ç¨¿è€…ã®æŠ•ç¨¿è€…自身ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã„å ´åˆ if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; @@ -100,13 +102,6 @@ class HybridTimelineChannel extends Channel { if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã™ã¹ãNoteã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ - // TODO: å°†æ¥çš„ã«ã¯ã€å˜ã«MutedNoteテーブルã«ãƒ¬ã‚³ãƒ¼ãƒ‰ãŒã‚ã‚‹ã‹ã©ã†ã‹ã§åˆ¤å®šã—ãŸã„(以下ã®ç†ç”±ã«ã‚ˆã‚Šé›£ã—ãã†ã§ã¯ã‚ã‚‹) - // ç¾çŠ¶ã§ã¯ã€ãƒ¯ãƒ¼ãƒ‰ãƒŸãƒ¥ãƒ¼ãƒˆã«ãŠã‘ã‚‹MutedNoteレコードã®è¿½åŠ 処ç†ã¯ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã«æµã™å‡¦ç†ã¨ä¸¦åˆ—ã§è¡Œã‚れるãŸã‚〠- // レコードãŒè¿½åŠ ã•ã‚Œã‚‹Noteã§ã‚‚è¿½åŠ ã•ã‚Œã‚‹ã‚ˆã‚Šå…ˆã«ã“ã“ã®ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã®å‡¦ç†ã«åˆ°é”ã™ã‚‹ã“ã¨ãŒèµ·ã“る。 - // ãã®ãŸã‚レコードãŒå˜åœ¨ã™ã‚‹ã‹ã®ãƒã‚§ãƒƒã‚¯ã§ã¯ä¸å分ãªã®ã§ã€æ”¹ã‚ã¦checkWordMuteを呼んã§ã„ã‚‹ - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - this.connection.cacheNote(note); this.send('note', note); diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index ef708c4fee86fe4f922387973e5393d3e69d65a3..94c22f8915afeb7b0a42c4cf0e4994905d885cce 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -15,10 +15,10 @@ import Channel from '../channel.js'; class LocalTimelineChannel extends Channel { public readonly chName = 'localTimeline'; - public static shouldShare = true; + public static shouldShare = false; public static requireCredential = false; - private withReplies: boolean; private withRenotes: boolean; + private withFiles: boolean; constructor( private metaService: MetaService, @@ -37,8 +37,8 @@ class LocalTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; - this.withReplies = params.withReplies ?? false; this.withRenotes = params.withRenotes ?? true; + this.withFiles = params.withFiles ?? false; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -46,6 +46,8 @@ class LocalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { + if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + if (note.user.host !== null) return; if (note.visibility !== 'public') return; if (note.channelId != null && !this.followingChannels.has(note.channelId)) return; @@ -64,7 +66,7 @@ class LocalTimelineChannel extends Channel { } // 関係ãªã„返信ã¯é™¤å¤– - if (note.reply && this.user && !this.withReplies) { + if (note.reply && this.user && !this.following[note.userId]?.withReplies) { const reply = note.reply; // 「ãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€Œãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ãŒè¡Œã£ãŸè¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€ŒæŠ•ç¨¿è€…ã®æŠ•ç¨¿è€…自身ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã„å ´åˆ if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; @@ -79,13 +81,6 @@ class LocalTimelineChannel extends Channel { if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã™ã¹ãNoteã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ - // TODO: å°†æ¥çš„ã«ã¯ã€å˜ã«MutedNoteテーブルã«ãƒ¬ã‚³ãƒ¼ãƒ‰ãŒã‚ã‚‹ã‹ã©ã†ã‹ã§åˆ¤å®šã—ãŸã„(以下ã®ç†ç”±ã«ã‚ˆã‚Šé›£ã—ãã†ã§ã¯ã‚ã‚‹) - // ç¾çŠ¶ã§ã¯ã€ãƒ¯ãƒ¼ãƒ‰ãƒŸãƒ¥ãƒ¼ãƒˆã«ãŠã‘ã‚‹MutedNoteレコードã®è¿½åŠ 処ç†ã¯ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã«æµã™å‡¦ç†ã¨ä¸¦åˆ—ã§è¡Œã‚れるãŸã‚〠- // レコードãŒè¿½åŠ ã•ã‚Œã‚‹Noteã§ã‚‚è¿½åŠ ã•ã‚Œã‚‹ã‚ˆã‚Šå…ˆã«ã“ã“ã®ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã®å‡¦ç†ã«åˆ°é”ã™ã‚‹ã“ã¨ãŒèµ·ã“る。 - // ãã®ãŸã‚レコードãŒå˜åœ¨ã™ã‚‹ã‹ã®ãƒã‚§ãƒƒã‚¯ã§ã¯ä¸å分ãªã®ã§ã€æ”¹ã‚ã¦checkWordMuteを呼んã§ã„ã‚‹ - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - this.connection.cacheNote(note); this.send('note', note); diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 8bbba0b6dbb917c5106f1309a9d5a56730b86434..240822d9ab559fdac7fee96220a3672833e25f64 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js'; +import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; @@ -18,12 +18,13 @@ class UserListChannel extends Channel { public static shouldShare = false; public static requireCredential = false; private listId: string; - public listUsers: MiUser['id'][] = []; + private membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {}; private listUsersClock: NodeJS.Timeout; + private withFiles: boolean; constructor( private userListsRepository: UserListsRepository, - private userListJoiningsRepository: UserListJoiningsRepository, + private userListMembershipsRepository: UserListMembershipsRepository, private noteEntityService: NoteEntityService, id: string, @@ -37,6 +38,7 @@ class UserListChannel extends Channel { @bindThis public async init(params: any) { this.listId = params.listId as string; + this.withFiles = params.withFiles ?? false; // Check existence and owner const listExist = await this.userListsRepository.exist({ @@ -58,19 +60,27 @@ class UserListChannel extends Channel { @bindThis private async updateListUsers() { - const users = await this.userListJoiningsRepository.find({ + const memberships = await this.userListMembershipsRepository.find({ where: { userListId: this.listId, }, select: ['userId'], }); - this.listUsers = users.map(x => x.userId); + const membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {}; + for (const membership of memberships) { + membershipsMap[membership.userId] = { + withReplies: membership.withReplies, + }; + } + this.membershipsMap = membershipsMap; } @bindThis private async onNote(note: Packed<'Note'>) { - if (!this.listUsers.includes(note.userId)) return; + if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + + if (!Object.hasOwn(this.membershipsMap, note.userId)) return; if (['followers', 'specified'].includes(note.visibility)) { note = await this.noteEntityService.pack(note.id, this.user, { @@ -95,6 +105,13 @@ class UserListChannel extends Channel { } } + // 関係ãªã„返信ã¯é™¤å¤– + if (note.reply && !this.membershipsMap[note.userId]?.withReplies) { + const reply = note.reply; + // 「ãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€Œãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ãŒè¡Œã£ãŸè¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€ŒæŠ•ç¨¿è€…ã®æŠ•ç¨¿è€…自身ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã„å ´åˆ + if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; + } + // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // æµã‚Œã¦ããŸNoteãŒãƒ–ãƒãƒƒã‚¯ã•ã‚Œã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ @@ -124,8 +141,8 @@ export class UserListChannelService { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private noteEntityService: NoteEntityService, ) { @@ -135,7 +152,7 @@ export class UserListChannelService { public create(id: string, connection: Channel['connection']): UserListChannel { return new UserListChannel( this.userListsRepository, - this.userListJoiningsRepository, + this.userListMembershipsRepository, this.noteEntityService, id, connection, diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 5c13c2c8701b8405b22527b63ab56f1d1a83f797..cf621f457936f9cf5533475ed96cfb0f62fa1733 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -188,7 +188,7 @@ export class ClientServerService { // Authenticate fastify.addHook('onRequest', async (request, reply) => { // %71ueueã¨ã‹ã§ãƒªã‚¯ã‚¨ã‚¹ãƒˆã•ã‚ŒãŸã‚‰å›°ã‚‹ãŸã‚ - const url = decodeURI(request.routerPath); + const url = decodeURI(request.routeOptions.url); if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) { const token = request.cookies.token; if (token == null) { diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index a9b9a55bc085c79140584ecec319e7fdb62ed092..316073c992b10a1d0ef616394fa7f0bfb2d4ba56 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -171,6 +171,9 @@ export type ModerationLogPayloads = { deleteUserAnnouncement: { announcementId: string; announcement: any; + userId: string; + userUsername: string; + userHost: string | null; }; resetPassword: { userId: string; diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts index c9e1ccc30410700e9740c9997d38ed3c77bc5735..7d57ba17b6fbd386f8ab6f4691af36b202638e39 100644 --- a/packages/backend/test/e2e/renote-mute.ts +++ b/packages/backend/test/e2e/renote-mute.ts @@ -6,7 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, react, startServer, waitFire } from '../utils.js'; +import { signup, api, post, react, startServer, waitFire, sleep } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; @@ -42,6 +42,9 @@ describe('Renote Mute', () => { const carolRenote = await post(carol, { renoteId: bobNote.id }); const carolNote = await post(carol, { text: 'hi' }); + // redisã«è¿½åŠ ã•ã‚Œã‚‹ã®ã‚’待㤠+ await sleep(100); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); @@ -56,6 +59,9 @@ describe('Renote Mute', () => { const carolRenote = await post(carol, { renoteId: bobNote.id, text: 'kore' }); const carolNote = await post(carol, { text: 'hi' }); + // redisã«è¿½åŠ ã•ã‚Œã‚‹ã®ã‚’待㤠+ await sleep(100); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts new file mode 100644 index 0000000000000000000000000000000000000000..05209c902443e941d11c5ad058937c5c7afaac69 --- /dev/null +++ b/packages/backend/test/e2e/timelines.ts @@ -0,0 +1,1097 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// How to run: +// pnpm jest -- e2e/timelines.ts + +process.env.NODE_ENV = 'test'; +process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true'; + +import * as assert from 'assert'; +import { signup, api, post, react, startServer, waitFire, sleep, uploadUrl, randomString } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; + +function genHost() { + return randomString() + '.example.com'; +} + +function waitForPushToTl() { + return sleep(300); +} + +let app: INestApplicationContext; + +beforeAll(async () => { + app = await startServer(); +}, 1000 * 60 * 2); + +afterAll(async () => { + await app.close(); +}); + +describe('Timelines', () => { + describe('Home TL', () => { + test.concurrent('自分㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice] = await Promise.all([signup()]); + + const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi' }); + const carolNote = await post(carol, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザー㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + const carolNote = await post(carol, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: false ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã¸ã®DM返信ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人㮠visibility: followers ãªæŠ•ç¨¿ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®è¡Œã£ãŸåˆ¥ã®ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザー㮠visibility: followers ãªæŠ•ç¨¿ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/create', { userId: carol.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id).text, 'hi'); + }); + + test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®è¡Œã£ãŸåˆ¥ã®ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®æŠ•ç¨¿ã¸ã® visibility: specified ãªè¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/create', { userId: carol.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + }); + + test.concurrent('withReplies: false ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®ãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼è‡ªèº«ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }); + + test.concurrent('自分ã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi' }); + const aliceNote = await post(alice, { text: 'hi', replyId: bobNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã®æŠ•ç¨¿ã®ãƒªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { renoteId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('[withRenotes: false] フォãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã®æŠ•ç¨¿ã®ãƒªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { renoteId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', { + withRenotes: false, + }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('[withRenotes: false] フォãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã®æŠ•ç¨¿ã®å¼•ç”¨ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', { + withRenotes: false, + }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã¸ã® visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーãŒè¡Œã£ãŸãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®ãƒªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーãŒè¡Œã£ãŸãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®æŠ•ç¨¿ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るリモートユーザーã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るリモートユーザー㮠visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('[withFiles: true] フォãƒãƒ¼ã—ã¦ã„るユーザーã®ãƒ•ã‚¡ã‚¤ãƒ«ä»˜ãノートã®ã¿å«ã¾ã‚Œã‚‹', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const [bobFile, carolFile] = await Promise.all([ + uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'), + uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'), + ]); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [bobFile.id] }); + const carolNote1 = await post(carol, { text: 'hi' }); + const carolNote2 = await post(carol, { fileIds: [carolFile.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', { withFiles: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote2.id), false); + }, 1000 * 10); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーã®ãƒãƒ£ãƒ³ãƒãƒ«æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('自分㮠visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice] = await Promise.all([signup()]); + + const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーã®è‡ªèº«ã‚’ visibleUserIds ã«æŒ‡å®šã—㟠visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã®è‡ªèº«ã‚’ visibleUserIds ã«æŒ‡å®šã—㟠visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーã®è‡ªèº«ã‚’ visibleUserIds ã«æŒ‡å®šã—ã¦ã„ãªã„ visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã‹ã‚‰ã® visibility: specified ãªãƒŽãƒ¼ãƒˆã«è¿”ä¿¡ã—ãŸã¨ãã®è‡ªèº«ã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); + const aliceNote = await post(alice, { text: 'ok', visibility: 'specified', visibleUserIds: [bob.id], replyId: bobNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'ok'); + }); + + /* TODO + test.concurrent('自身㮠visibility: specified ãªãƒŽãƒ¼ãƒˆã¸ã®ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã‹ã‚‰ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const aliceNote = await post(alice, { text: 'hi', visibility: 'specified', visibleUserIds: [bob.id] }); + const bobNote = await post(bob, { text: 'ok', visibility: 'specified', visibleUserIds: [alice.id], replyId: aliceNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'ok'); + }); + */ + + // ↑ã®æŒ™å‹•ãŒç†æƒ³ã ã‘ã©å®Ÿè£…ãŒé¢å€’ã‹ã‚‚ + test.concurrent('自身㮠visibility: specified ãªãƒŽãƒ¼ãƒˆã¸ã®ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã‹ã‚‰ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const aliceNote = await post(alice, { text: 'hi', visibility: 'specified', visibleUserIds: [bob.id] }); + const bobNote = await post(bob, { text: 'ok', visibility: 'specified', visibleUserIds: [alice.id], replyId: aliceNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + }); + + describe('Local TL', () => { + test.concurrent('visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('ãƒãƒ£ãƒ³ãƒãƒ«æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('リモートユーザーã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + // å«ã¾ã‚Œã¦ã‚‚良ã„ã¨æ€ã†ã‘ã©å®Ÿè£…ãŒé¢å€’ãªã®ã§å«ã¾ã‚Œãªã„ + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザー㮠visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('ミュートã—ã¦ã„るユーザーã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーãŒè¡Œã£ãŸãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®ãƒªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーãŒè¡Œã£ãŸãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®æŠ•ç¨¿ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('[withFiles: true] ファイル付ãノートã®ã¿å«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [file.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', { withFiles: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }, 1000 * 10); + }); + + describe('Social TL', () => { + test.concurrent('ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/hybrid-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã® visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + + await waitForPushToTl(); + + const res = await api('/notes/hybrid-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„ã‚‹ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã® visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + + await waitForPushToTl(); + + const res = await api('/notes/hybrid-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('リモートユーザーã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るリモートユーザーã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/hybrid-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るリモートユーザー㮠visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + + await waitForPushToTl(); + + const res = await api('/notes/hybrid-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('[withFiles: true] ファイル付ãノートã®ã¿å«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [file.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/hybrid-timeline', { withFiles: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }, 1000 * 10); + }); + + describe('User List TL', () => { + test.concurrent('リスインã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('リスインã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„ãªã„ユーザー㮠visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('リスインã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„ãªã„ユーザー㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('リスインã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('リスインã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã®ãƒ¦ãƒ¼ã‚¶ãƒ¼è‡ªèº«ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }); + + test.concurrent('withReplies: true ã§ãƒªã‚¹ã‚¤ãƒ³ã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('/users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('リスインã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„るユーザー㮠visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('リスインã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„るユーザー㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); + }); + + test.concurrent('リスインã—ã¦ã„るユーザーã®ãƒãƒ£ãƒ³ãƒãƒ«ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('[withFiles: true] リスインã—ã¦ã„るユーザーã®ãƒ•ã‚¡ã‚¤ãƒ«ä»˜ãノートã®ã¿å«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [file.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id, withFiles: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }, 1000 * 10); + + test.concurrent('リスインã—ã¦ã„るユーザーã®è‡ªèº«å®›ã¦ã® visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); + }); + + test.concurrent('リスインã—ã¦ã„るユーザーã®è‡ªèº«å®›ã¦ã§ã¯ãªã„ visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('/users/lists/push', { listId: list.id, userId: carol.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + }); + + describe('User TL', () => { + test.concurrent('ノートãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„ãªã„ユーザー㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザー㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); + }); + + test.concurrent('自身㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice] = await Promise.all([signup()]); + + const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: alice.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); + }); + + test.concurrent('ãƒãƒ£ãƒ³ãƒãƒ«æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('[withReplies: false] 他人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const carolNote = await post(carol, { text: 'hi' }); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false); + }); + + test.concurrent('[withReplies: true] 他人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const carolNote = await post(carol, { text: 'hi' }); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }); + + test.concurrent('[withReplies: true] 他人ã¸ã® visibility: specified ãªè¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const carolNote = await post(carol, { text: 'hi' }); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified' }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false); + }); + + test.concurrent('[withFiles: true] ファイル付ãノートã®ã¿å«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [file.id] }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id, withFiles: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }, 1000 * 10); + + test.concurrent('[withChannelNotes: true] ãƒãƒ£ãƒ³ãƒãƒ«æŠ•ç¨¿ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id, withChannelNotes: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('ミュートã—ã¦ã„るユーザーã«é–¢é€£ã™ã‚‹æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('ミュートã—ã¦ã„ã¦ã‚‚ userId ã«æŒ‡å®šã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('/mute/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); + const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote3.id), true); + }); + + test.concurrent('自身㮠visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice] = await Promise.all([signup()]); + + const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: alice.id, withReplies: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + }); + + test.concurrent('visibleUserIds ã«æŒ‡å®šã•ã‚Œã¦ãªã„ visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi', visibility: 'specified' }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + }); + + // TODO: リノートミュート済ã¿ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ†ã‚¹ãƒˆ + // TODO: ページãƒãƒ¼ã‚·ãƒ§ãƒ³ã®ãƒ†ã‚¹ãƒˆ +}); diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts index 121070787d40f5174fac76476299b9caffbcdfcc..b5f00a632752acf3ddd082756e3fff182eccabe4 100644 --- a/packages/backend/test/e2e/user-notes.ts +++ b/packages/backend/test/e2e/user-notes.ts @@ -38,23 +38,10 @@ describe('users/notes', () => { await app.close(); }); - test('ファイルタイプ指定 (jpg)', async () => { + test('withFiles', async () => { const res = await api('/users/notes', { userId: alice.id, - fileType: ['image/jpeg'], - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 2); - assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); - }); - - test('ファイルタイプ指定 (jpg or png)', async () => { - const res = await api('/users/notes', { - userId: alice.id, - fileType: ['image/jpeg', 'image/png'], + withFiles: true, }, alice); assert.strictEqual(res.status, 200); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 0f5d5f734466ab291aa91a5079835ec785470818..53db1ac28a006a9dfed96bd7745acc4ce42afbb7 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -133,6 +133,7 @@ describe('ユーザー', () => { isMuted: user.isMuted ?? false, isRenoteMuted: user.isRenoteMuted ?? false, notify: user.notify ?? 'none', + withReplies: user.withReplies ?? false, }); }; diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index dbc446d12de01d4a512b6374c7a498245513e3c3..2e9454927c7f60e21874393b64cf3e47594e5f9d 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -92,6 +92,9 @@ describe('ActivityPub', () => { const metaInitial = { cacheRemoteFiles: true, cacheRemoteSensitiveFiles: true, + perUserHomeTimelineCacheMax: 100, + perLocalUserUserTimelineCacheMax: 100, + perRemoteUserUserTimelineCacheMax: 100, blockedHosts: [] as string[], sensitiveWords: [] as string[], } as MiMeta; diff --git a/packages/backend/test/unit/misc/loader.ts b/packages/backend/test/unit/misc/loader.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa37950951181e06c8dc7803cec1508198ec1d4f --- /dev/null +++ b/packages/backend/test/unit/misc/loader.ts @@ -0,0 +1,88 @@ +import { DebounceLoader } from '@/misc/loader.js'; + +class Mock { + loadCountByKey = new Map<number, number>(); + load = async (key: number): Promise<number> => { + const count = this.loadCountByKey.get(key); + if (typeof count === 'undefined') { + this.loadCountByKey.set(key, 1); + } else { + this.loadCountByKey.set(key, count + 1); + } + return key * 2; + }; + reset() { + this.loadCountByKey.clear(); + } +} + +describe(DebounceLoader, () => { + describe('single request', () => { + it('loads once', async () => { + const mock = new Mock(); + const loader = new DebounceLoader(mock.load); + expect(await loader.load(7)).toBe(14); + expect(mock.loadCountByKey.size).toBe(1); + expect(mock.loadCountByKey.get(7)).toBe(1); + }); + }); + + describe('two duplicated requests at same time', () => { + it('loads once', async () => { + const mock = new Mock(); + const loader = new DebounceLoader(mock.load); + const [v1, v2] = await Promise.all([ + loader.load(7), + loader.load(7), + ]); + expect(v1).toBe(14); + expect(v2).toBe(14); + expect(mock.loadCountByKey.size).toBe(1); + expect(mock.loadCountByKey.get(7)).toBe(1); + }); + }); + + describe('two different requests at same time', () => { + it('loads twice', async () => { + const mock = new Mock(); + const loader = new DebounceLoader(mock.load); + const [v1, v2] = await Promise.all([ + loader.load(7), + loader.load(13), + ]); + expect(v1).toBe(14); + expect(v2).toBe(26); + expect(mock.loadCountByKey.size).toBe(2); + expect(mock.loadCountByKey.get(7)).toBe(1); + expect(mock.loadCountByKey.get(13)).toBe(1); + }); + }); + + describe('non-continuous same two requests', () => { + it('loads twice', async () => { + const mock = new Mock(); + const loader = new DebounceLoader(mock.load); + expect(await loader.load(7)).toBe(14); + expect(mock.loadCountByKey.size).toBe(1); + expect(mock.loadCountByKey.get(7)).toBe(1); + mock.reset(); + expect(await loader.load(7)).toBe(14); + expect(mock.loadCountByKey.size).toBe(1); + expect(mock.loadCountByKey.get(7)).toBe(1); + }); + }); + + describe('non-continuous different two requests', () => { + it('loads twice', async () => { + const mock = new Mock(); + const loader = new DebounceLoader(mock.load); + expect(await loader.load(7)).toBe(14); + expect(mock.loadCountByKey.size).toBe(1); + expect(mock.loadCountByKey.get(7)).toBe(1); + mock.reset(); + expect(await loader.load(13)).toBe(26); + expect(mock.loadCountByKey.size).toBe(1); + expect(mock.loadCountByKey.get(13)).toBe(1); + }); + }); +}); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index adc532bbe7bcdfe373d9e01ef340f5772cab54b9..97118d73c0e1702648b13e66ea878540410c89c4 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -99,9 +99,17 @@ export const relativeFetch = async (path: string, init?: RequestInit | undefined return await fetch(new URL(path, `http://127.0.0.1:${port}/`).toString(), init); }; +export function randomString(chars = 'abcdefghijklmnopqrstuvwxyz0123456789', length = 16) { + let randomString = ''; + for (let i = 0; i < length; i++) { + randomString += chars[Math.floor(Math.random() * chars.length)]; + } + return randomString; +} + export const signup = async (params?: Partial<misskey.Endpoints['signup']['req']>): Promise<NonNullable<misskey.Endpoints['signup']['res']>> => { const q = Object.assign({ - username: 'test', + username: randomString(), password: 'test', }, params); @@ -293,12 +301,14 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO }; export const uploadUrl = async (user: UserToken, url: string) => { - let file: any; + let resolve: unknown; + const file = new Promise(ok => resolve = ok); const marker = Math.random().toString(); const ws = await connectStream(user, 'main', (msg) => { if (msg.type === 'urlUploadFinished' && msg.body.marker === marker) { - file = msg.body.file; + ws.close(); + resolve(msg.body.file); } }); @@ -308,9 +318,6 @@ export const uploadUrl = async (user: UserToken, url: string) => { force: true, }, user); - await sleep(7000); - ws.close(); - return file; }; @@ -450,6 +457,7 @@ export async function testPaginationConsistency<Entity extends { id: string, cre }; for (const limit of [1, 5, 10, 100, undefined]) { + /* // 1. sinceId/Dateã¨untilId/Dateã§ä¸¡ç«¯ã‚’指定ã—ã¦å–å¾—ã—ãŸçµæžœãŒæœŸå¾…通りã«ãªã£ã¦ã„ã‚‹ã“㨠if (ordering === 'desc') { const end = expected.at(-1)!; @@ -478,6 +486,7 @@ export async function testPaginationConsistency<Entity extends { id: string, cre actual.map(({ id, createdAt }) => id + ':' + createdAt), expected.map(({ id, createdAt }) => id + ':' + createdAt)); } + */ // 3. untilId指定+limitã§å–å¾—ã—ã¦ã¤ãªãŽåˆã‚ã›ãŸçµæžœãŒæœŸå¾…通りã«ãªã£ã¦ã„ã‚‹ã“㨠if (ordering === 'desc') { diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 1f704cd2585af9c00290a4d8527ec627f824e866..8b7604f311a2440966a9791e0027a35857a9c4cc 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -18,13 +18,13 @@ "dependencies": { "@discordapp/twemoji": "14.1.2", "@github/webauthn-json": "2.1.1", - "@rollup/plugin-alias": "5.0.0", - "@rollup/plugin-json": "6.0.0", - "@rollup/plugin-replace": "5.0.2", - "@rollup/pluginutils": "5.0.4", + "@rollup/plugin-alias": "5.0.1", + "@rollup/plugin-json": "6.0.1", + "@rollup/plugin-replace": "5.0.3", + "@rollup/pluginutils": "5.0.5", "@syuilo/aiscript": "0.16.0", "@tabler/icons-webfont": "2.37.0", - "@vitejs/plugin-vue": "4.3.4", + "@vitejs/plugin-vue": "4.4.0", "@vue-macros/reactivity-transform": "0.3.23", "@vue/compiler-sfc": "3.3.4", "astring": "1.8.6", @@ -38,7 +38,7 @@ "chartjs-chart-matrix": "2.0.1", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.0.1", - "chromatic": "7.2.0", + "chromatic": "7.2.3", "compare-versions": "6.1.0", "cropperjs": "2.0.0-beta.4", "date-fns": "2.30.0", @@ -53,13 +53,13 @@ "matter-js": "0.19.0", "mfm-js": "0.23.3", "misskey-js": "workspace:*", - "photoswipe": "5.4.1", + "photoswipe": "5.4.2", "prismjs": "1.29.0", "punycode": "2.3.0", "querystring": "0.2.1", - "rollup": "3.29.4", + "rollup": "4.0.2", "sanitize-html": "2.11.0", - "sass": "1.68.0", + "sass": "1.69.1", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", "three": "0.157.0", @@ -72,70 +72,70 @@ "uuid": "9.0.1", "v-code-diff": "1.7.1", "vanilla-tilt": "1.8.1", - "vite": "4.4.9", + "vite": "4.4.11", "vue": "3.3.4", "vue-prism-editor": "2.0.0-alpha.2", "vuedraggable": "next" }, "devDependencies": { - "@storybook/addon-actions": "7.4.5", - "@storybook/addon-essentials": "7.4.5", - "@storybook/addon-interactions": "7.4.5", - "@storybook/addon-links": "7.4.5", - "@storybook/addon-storysource": "7.4.5", - "@storybook/addons": "7.4.5", - "@storybook/blocks": "7.4.5", - "@storybook/core-events": "7.4.5", - "@storybook/jest": "0.2.2", - "@storybook/manager-api": "7.4.5", - "@storybook/preview-api": "7.4.5", - "@storybook/react": "7.4.5", - "@storybook/react-vite": "7.4.5", - "@storybook/testing-library": "0.2.1", - "@storybook/theming": "7.4.5", - "@storybook/types": "7.4.5", - "@storybook/vue3": "7.4.5", - "@storybook/vue3-vite": "7.4.5", + "@storybook/addon-actions": "7.4.6", + "@storybook/addon-essentials": "7.4.6", + "@storybook/addon-interactions": "7.4.6", + "@storybook/addon-links": "7.4.6", + "@storybook/addon-storysource": "7.4.6", + "@storybook/addons": "7.4.6", + "@storybook/blocks": "7.4.6", + "@storybook/core-events": "7.4.6", + "@storybook/jest": "0.2.3", + "@storybook/manager-api": "7.4.6", + "@storybook/preview-api": "7.4.6", + "@storybook/react": "7.4.6", + "@storybook/react-vite": "7.4.6", + "@storybook/testing-library": "0.2.2", + "@storybook/theming": "7.4.6", + "@storybook/types": "7.4.6", + "@storybook/vue3": "7.4.6", + "@storybook/vue3-vite": "7.4.6", "@testing-library/vue": "7.0.0", "@types/escape-regexp": "0.0.1", "@types/estree": "1.0.2", "@types/matter-js": "0.19.1", "@types/micromatch": "4.0.3", - "@types/node": "20.7.1", + "@types/node": "20.8.4", "@types/punycode": "2.1.0", "@types/sanitize-html": "2.9.1", "@types/throttle-debounce": "5.0.0", "@types/tinycolor2": "1.4.4", - "@types/uuid": "9.0.4", + "@types/uuid": "9.0.5", "@types/websocket": "1.0.7", "@types/ws": "8.5.6", - "@typescript-eslint/eslint-plugin": "6.7.3", - "@typescript-eslint/parser": "6.7.3", - "@vitest/coverage-v8": "0.34.5", + "@typescript-eslint/eslint-plugin": "6.7.5", + "@typescript-eslint/parser": "6.7.5", + "@vitest/coverage-v8": "0.34.6", "@vue/runtime-core": "3.3.4", "acorn": "8.10.0", "cross-env": "7.0.3", "cypress": "13.3.0", - "eslint": "8.50.0", + "eslint": "8.51.0", "eslint-plugin-import": "2.28.1", "eslint-plugin-vue": "9.17.0", "fast-glob": "3.3.1", "happy-dom": "10.0.3", "micromatch": "4.0.5", - "msw": "1.3.1", + "msw": "1.3.2", "msw-storybook-addon": "1.8.0", "nodemon": "3.0.1", "prettier": "3.0.3", "react": "18.2.0", "react-dom": "18.2.0", "start-server-and-test": "2.0.1", - "storybook": "7.4.5", + "storybook": "7.4.6", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "summaly": "github:misskey-dev/summaly", "vite-plugin-turbosnap": "1.0.3", - "vitest": "0.34.5", + "vitest": "0.34.6", "vitest-fetch-mock": "0.2.2", - "vue-eslint-parser": "9.3.1", - "vue-tsc": "1.8.15" + "vue-eslint-parser": "9.3.2", + "vue-tsc": "1.8.18" } } diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue index 54c215935628f8cf69eaff074812d5798e5502a8..0cdaf7c9bda51516ac4deb8808aabf4d4161bd72 100644 --- a/packages/frontend/src/components/MkCwButton.vue +++ b/packages/frontend/src/components/MkCwButton.vue @@ -4,10 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<button class="_button" :class="$style.root" @mousedown="toggle"> - <b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b> - <span v-if="!modelValue" :class="$style.label">{{ label }}</span> -</button> +<MkButton rounded full small @click="toggle"><b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b><span v-if="!modelValue" :class="$style.label">{{ label }}</span></MkButton> </template> <script lang="ts" setup> @@ -15,6 +12,7 @@ import { computed } from 'vue'; import * as Misskey from 'misskey-js'; import { concat } from '@/scripts/array.js'; import { i18n } from '@/i18n.js'; +import MkButton from '@/components/MkButton.vue'; const props = defineProps<{ modelValue: boolean; @@ -33,25 +31,12 @@ const label = computed(() => { ] as string[][]).join(' / '); }); -const toggle = () => { +function toggle() { emit('update:modelValue', !props.modelValue); -}; +} </script> <style lang="scss" module> -.root { - display: inline-block; - padding: 4px 8px; - font-size: 0.7em; - color: var(--cwFg); - background: var(--cwBg); - border-radius: 2px; - - &:hover { - background: var(--cwHoverBg); - } -} - .label { margin-left: 4px; diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index e3f96724d9dcd024b01f333d2aa79ea9c450df4f..96704996f9c3c602b7b788b81ecf29c9482bf45a 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -45,8 +45,11 @@ import bytes from '@/filters/bytes.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; +import { useRouter } from '@/router.js'; import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js'; +const router = useRouter(); + const props = withDefaults(defineProps<{ file: Misskey.entities.DriveFile; folder: Misskey.entities.DriveFolder | null; @@ -71,7 +74,7 @@ function onClick(ev: MouseEvent) { if (props.selectMode) { emit('chosen', props.file); } else { - os.popupMenu(getDriveFileMenu(props.file, props.folder), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + router.push(`/my/drive/file/${props.file.id}`); } } diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index 315ce958c5a7f0c14b82fb23aef88cbe0feac2c8..5cfd7eb534aa97408aeaf166862840ebf89cfac3 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -23,6 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only :spellcheck="spellcheck" :step="step" :list="id" + :min="min" + :max="max" @focus="focused = true" @blur="focused = false" @keydown="onKeydown($event)" @@ -59,6 +61,8 @@ const props = defineProps<{ spellcheck?: boolean; step?: any; datalist?: string[]; + min?: number; + max?: number; inline?: boolean; debounce?: boolean; manualSave?: boolean; diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 10b2ac9ece16d27267e74952d5b1e783ff46ba7c..69da1a7466847a28bb4a718b6b776614da7cd70c 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -17,7 +17,6 @@ SPDX-License-Identifier: AGPL-3.0-only :title="media.name" controls preload="metadata" - @volumechange="volumechange" /> </div> <a @@ -33,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { soundConfigStore } from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; @@ -43,15 +42,13 @@ const props = withDefaults(defineProps<{ }>(), { }); -const audioEl = $shallowRef<HTMLAudioElement | null>(); +const audioEl = shallowRef<HTMLAudioElement>(); let hide = $ref(true); -function volumechange() { - if (audioEl) soundConfigStore.set('mediaVolume', audioEl.volume); -} - -onMounted(() => { - if (audioEl) audioEl.volume = soundConfigStore.state.mediaVolume; +watch(audioEl, () => { + if (audioEl.value) { + audioEl.value.volume = 0.3; + } }); </script> diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 751b5f75700a712bdba9bde3e661b57e88456fd2..43c64b4c85cb33e51f221e5256d83ca9f7e33995 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-else :class="[$style.visible, (video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitiveContainer]"> <video + ref="videoEl" :class="$style.video" :poster="video.thumbnailUrl" :title="video.comment" @@ -31,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { ref, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import bytes from '@/filters/bytes.js'; import { defaultStore } from '@/store.js'; @@ -42,6 +43,14 @@ const props = defineProps<{ }>(); const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore')); + +const videoEl = shallowRef<HTMLVideoElement>(); + +watch(videoEl, () => { + if (videoEl.value) { + videoEl.value.volume = 0.3; + } +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index b397f3eee9f02dfb4c0a9a3612fbf7fd09099641..5272bf865e4c24734da599e84024189d51102645 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div style="container-type: inline-size;"> <p v-if="appearNote.cw != null" :class="$style.cw"> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/> - <MkCwButton v-model="showContent" :note="appearNote"/> + <MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;"/> </p> <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> <div :class="$style.text"> @@ -165,7 +165,7 @@ import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; -import { MenuItem } from '@/types/menu'; +import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { shouldCollapsed } from '@/scripts/collapsed.js'; @@ -211,11 +211,11 @@ const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : n const isLong = shouldCollapsed(appearNote); const collapsed = ref(appearNote.cw == null && isLong); const isDeleted = ref(false); -const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords)); +const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); const translation = ref<any>(null); const translating = ref(false); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); -const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id); +const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id)); let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null))); const keymap = { diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index ab8886e8ba9f823fe6a7052a0015221de19e48c0..a1360aba9dc6c713a3dfdbe69f20c48d08dab9cb 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -93,9 +93,6 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <footer> <div :class="$style.noteFooterInfo"> - <div v-if="appearNote.updatedAt"> - {{ i18n.ts.edited }}: <MkTime :time="appearNote.updatedAt" mode="detail"/> - </div> <MkA :to="notePage(appearNote)"> <MkTime :time="appearNote.createdAt" mode="detail"/> </MkA> @@ -214,7 +211,7 @@ import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import { claimAchievement } from '@/scripts/achievements.js'; -import { MenuItem } from '@/types/menu'; +import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; @@ -258,7 +255,7 @@ let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note const isMyRenote = $i && ($i.id === note.userId); const showContent = ref(false); const isDeleted = ref(false); -const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords)); +const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); const translation = ref(null); const translating = ref(false); const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null; diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 05f98c638ef236057e1e6121cfeb84789ffe4233..dda7238d273bfe0eade74b8f99e6643f58b874bc 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -14,7 +14,6 @@ SPDX-License-Identifier: AGPL-3.0-only <img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/> </div> <div :class="$style.info"> - <span v-if="note.updatedAt" style="margin-right: 0.5em;" :title="i18n.ts.edited"><i class="ti ti-pencil"></i></span> <MkA :to="notePage(note)"> <MkTime :time="note.createdAt"/> </MkA> diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 2a3cd9bf022d16270fb2b98a882cdf86fe2e620f..bc52101f429e81a169d4029553bbf34e1f4df48a 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -49,9 +49,9 @@ import { notePage } from '@/filters/note.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { userPage } from "@/filters/user"; -import { checkWordMute } from "@/scripts/check-word-mute"; -import { defaultStore } from "@/store"; +import { userPage } from '@/filters/user.js'; +import { checkWordMute } from '@/scripts/check-word-mute.js'; +import { defaultStore } from '@/store.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -63,7 +63,7 @@ const props = withDefaults(defineProps<{ depth: 1, }); -const muted = ref(checkWordMute(props.note, $i, defaultStore.state.mutedWords)); +const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false); let showContent = $ref(false); let replies: Misskey.entities.Note[] = $ref([]); diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index b82ca3ef19f187f9c47fc7aa5d92e5f730efb481..f6981cea72f146157b7d1a7381933ca975f807c7 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -143,7 +143,6 @@ const props = withDefaults(defineProps<{ fixed?: boolean; autofocus?: boolean; freezeAfterPosted?: boolean; - updateMode?: boolean; }>(), { initialVisibleUsers: () => [], autofocus: true, @@ -710,7 +709,6 @@ async function post(ev?: MouseEvent) { visibility: visibility, visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined, reactionAcceptance, - noteId: props.updateMode ? props.initialNote?.id : undefined, }; if (withHashtags && hashtags && hashtags.trim() !== '') { @@ -733,7 +731,7 @@ async function post(ev?: MouseEvent) { } posting = true; - os.api(props.updateMode ? 'notes/update' : 'notes/create', postData, token).then(() => { + os.api('notes/create', postData, token).then(() => { if (props.freezeAfterPosted) { posted = true; } else { diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue index f33d498f93a8b6f8e754a163439196501cb87049..c07a166a83184cf23d6fef6fc48d595ac12090c9 100644 --- a/packages/frontend/src/components/MkPostFormDialog.vue +++ b/packages/frontend/src/components/MkPostFormDialog.vue @@ -30,7 +30,6 @@ const props = defineProps<{ instant?: boolean; fixed?: boolean; autofocus?: boolean; - updateMode?: boolean; }>(); const emit = defineEmits<{ diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue index aa4a184d7b8851179d9180a41d9fa43a96bbf22f..76163ab68b05f4cf6cd05dca734fe4fc62182622 100644 --- a/packages/frontend/src/components/MkSignupDialog.rules.vue +++ b/packages/frontend/src/components/MkSignupDialog.rules.vue @@ -30,13 +30,15 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch :modelValue="agreeServerRules" style="margin-top: 16px;" @update:modelValue="updateAgreeServerRules">{{ i18n.ts.agree }}</MkSwitch> </MkFolder> - <MkFolder v-if="availableTos" :defaultOpen="true"> - <template #label>{{ i18n.ts.termsOfService }}</template> - <template #suffix><i v-if="agreeTos" class="ti ti-check" style="color: var(--success)"></i></template> - - <a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a> - - <MkSwitch :modelValue="agreeTos" style="margin-top: 16px;" @update:modelValue="updateAgreeTos">{{ i18n.ts.agree }}</MkSwitch> + <MkFolder v-if="availableTos || availablePrivacyPolicy" :defaultOpen="true"> + <template #label>{{ tosPrivacyPolicyLabel }}</template> + <template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template> + <div class="_gaps_s"> + <div v-if="availableTos"><a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div> + <div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div> + </div> + + <MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch> </MkFolder> <MkFolder :defaultOpen="true"> @@ -70,14 +72,15 @@ import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; const availableServerRules = instance.serverRules.length > 0; -const availableTos = instance.tosUrl != null; +const availableTos = instance.tosUrl != null && instance.tosUrl !== ''; +const availablePrivacyPolicy = instance.privacyPolicyUrl != null && instance.privacyPolicyUrl !== ''; const agreeServerRules = ref(false); -const agreeTos = ref(false); +const agreeTosAndPrivacyPolicy = ref(false); const agreeNote = ref(false); const agreed = computed(() => { - return (!availableServerRules || agreeServerRules.value) && (!availableTos || agreeTos.value) && agreeNote.value; + return (!availableServerRules || agreeServerRules.value) && ((!availableTos && !availablePrivacyPolicy) || agreeTosAndPrivacyPolicy.value) && agreeNote.value; }); const emit = defineEmits<{ @@ -85,6 +88,18 @@ const emit = defineEmits<{ (ev: 'done'): void; }>(); +const tosPrivacyPolicyLabel = computed(() => { + if (availableTos && availablePrivacyPolicy) { + return i18n.ts.tosAndPrivacyPolicy; + } else if (availableTos) { + return i18n.ts.termsOfService; + } else if (availablePrivacyPolicy) { + return i18n.ts.privacyPolicy; + } else { + return ""; + } +}); + async function updateAgreeServerRules(v: boolean) { if (v) { const confirm = await os.confirm({ @@ -99,17 +114,19 @@ async function updateAgreeServerRules(v: boolean) { } } -async function updateAgreeTos(v: boolean) { +async function updateAgreeTosAndPrivacyPolicy(v: boolean) { if (v) { const confirm = await os.confirm({ type: 'question', title: i18n.ts.doYouAgree, - text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.termsOfService }), + text: i18n.t('iHaveReadXCarefullyAndAgree', { + x: tosPrivacyPolicyLabel.value, + }), }); if (confirm.canceled) return; - agreeTos.value = true; + agreeTosAndPrivacyPolicy.value = true; } else { - agreeTos.value = false; + agreeTosAndPrivacyPolicy.value = false; } } diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 1dcafd6be11790ddb400188add8069ff41814595..45dedd5042c1b0a9e9efb1ce4b25ed8ec6e94af3 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -13,6 +13,7 @@ import MkNotes from '@/components/MkNotes.vue'; import { useStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; import { $i } from '@/account.js'; +import { instance } from '@/instance.js'; import { defaultStore } from '@/store.js'; const props = withDefaults(defineProps<{ @@ -23,11 +24,9 @@ const props = withDefaults(defineProps<{ role?: string; sound?: boolean; withRenotes?: boolean; - withReplies?: boolean; onlyFiles?: boolean; }>(), { withRenotes: true, - withReplies: false, onlyFiles: false, }); @@ -40,7 +39,15 @@ provide('inChannel', computed(() => props.src === 'channel')); const tlComponent: InstanceType<typeof MkNotes> = $ref(); +let tlNotesCount = 0; + const prepend = note => { + tlNotesCount++; + + if (instance.notesPerOneAd > 0 && tlNotesCount % instance.notesPerOneAd === 0) { + note._shouldInsertAd_ = true; + } + tlComponent.pagingComponent?.prepend(note); emit('note'); @@ -70,12 +77,10 @@ if (props.src === 'antenna') { endpoint = 'notes/timeline'; query = { withRenotes: props.withRenotes, - withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, }; connection = stream.useChannel('homeTimeline', { withRenotes: props.withRenotes, - withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, }); connection.on('note', prepend); @@ -85,12 +90,10 @@ if (props.src === 'antenna') { endpoint = 'notes/local-timeline'; query = { withRenotes: props.withRenotes, - withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, }; connection = stream.useChannel('localTimeline', { withRenotes: props.withRenotes, - withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, }); connection.on('note', prepend); @@ -98,12 +101,10 @@ if (props.src === 'antenna') { endpoint = 'notes/hybrid-timeline'; query = { withRenotes: props.withRenotes, - withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, }; connection = stream.useChannel('hybridTimeline', { withRenotes: props.withRenotes, - withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, }); connection.on('note', prepend); @@ -111,12 +112,10 @@ if (props.src === 'antenna') { endpoint = 'notes/global-timeline'; query = { withRenotes: props.withRenotes, - withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, }; connection = stream.useChannel('globalTimeline', { withRenotes: props.withRenotes, - withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, }); connection.on('note', prepend); @@ -139,14 +138,10 @@ if (props.src === 'antenna') { } else if (props.src === 'list') { endpoint = 'notes/user-list-timeline'; query = { - withRenotes: props.withRenotes, - withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, listId: props.list, }; connection = stream.useChannel('userList', { - withRenotes: props.withRenotes, - withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, listId: props.list, }); diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index e4520bbb2d4a0311f95b3287b92546720416e728..40493a5d067a8ebf969f8d72de8d1c0fc2e4a0bc 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -104,7 +104,25 @@ function showMenu(ev) { action: () => { os.pageWindow('/about-misskey'); }, - }, null, { + }, null, (instance.impressumUrl) ? { + text: i18n.ts.impressum, + icon: 'ti ti-file-invoice', + action: () => { + window.open(instance.impressumUrl, '_blank'); + }, + } : undefined, (instance.tosUrl) ? { + text: i18n.ts.termsOfService, + icon: 'ti ti-notebook', + action: () => { + window.open(instance.tosUrl, '_blank'); + }, + } : undefined, (instance.privacyPolicyUrl) ? { + text: i18n.ts.privacyPolicy, + icon: 'ti ti-shield-lock', + action: () => { + window.open(instance.privacyPolicyUrl, '_blank'); + }, + } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : null, { text: i18n.ts.help, icon: 'ti ti-help-circle', action: () => { diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index f4ce0a096deaed08cfba0daf84507734fc9267e1..3998df9efe576b5cefc4d9b61264b020dc08a9a5 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -61,7 +61,6 @@ export const ROLE_POLICIES = [ 'gtlAvailable', 'ltlAvailable', 'canPublicNote', - 'canEditNote', 'canInvite', 'inviteLimit', 'inviteLimitCycle', diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 37c869f98ee884686b6fdc765bdacfe38225e4bb..f67697db5513a5eb6c11a6443f3a4f91c8d6fb1e 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -187,6 +187,9 @@ const patronsWithIcon = [{ }, { name: 'フランギ・シュウ', icon: 'https://misskey-hub.net/patrons/3016d37e35f3430b90420176c912d304.jpg', +}, { + name: '百日紅', + icon: 'https://misskey-hub.net/patrons/302dce2898dd457ba03c3f7dc037900b.jpg', }]; const patrons = [ @@ -287,6 +290,7 @@ const patrons = [ 'kino3277', '美少女JKãーã¡ã‚ƒã‚“', 'ã¦ã°', + 'ãŸã£ãã‚“', ]; let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure')); diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index 02768b07740645ae34cf3a8aef370ee02ea951b6..ee4043f9a500bd116acc3cf69b1706515c83e66b 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -46,14 +46,18 @@ SPDX-License-Identifier: AGPL-3.0-only <template #value>{{ instance.maintainerEmail }}</template> </MkKeyValue> </FormSplit> - <MkFolder v-if="instance.serverRules.length > 0"> - <template #label>{{ i18n.ts.serverRules }}</template> - - <ol class="_gaps_s" :class="$style.rules"> - <li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li> - </ol> - </MkFolder> - <FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink> + <FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink> + <div class="_formLinks"> + <MkFolder v-if="instance.serverRules.length > 0"> + <template #label>{{ i18n.ts.serverRules }}</template> + + <ol class="_gaps_s" :class="$style.rules"> + <li v-for="item, index in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li> + </ol> + </MkFolder> + <FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink> + <FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>{{ i18n.ts.privacyPolicy }}</FormLink> + </div> </div> </FormSection> diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue new file mode 100644 index 0000000000000000000000000000000000000000..e88860166c0525bae75dd5ccb4b68b99e0681170 --- /dev/null +++ b/packages/frontend/src/pages/admin/external-services.vue @@ -0,0 +1,81 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> + <FormSuspense :p="init"> + <FormSection> + <template #label>DeepL Translation</template> + + <div class="_gaps_m"> + <MkInput v-model="deeplAuthKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>DeepL Auth Key</template> + </MkInput> + <MkSwitch v-model="deeplIsPro"> + <template #label>Pro account</template> + </MkSwitch> + </div> + </FormSection> + </FormSuspense> + </MkSpacer> + <template #footer> + <div :class="$style.footer"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> + <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + </MkSpacer> + </div> + </template> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import XHeader from './_header_.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkButton from '@/components/MkButton.vue'; +import FormSuspense from '@/components/form/suspense.vue'; +import FormSection from '@/components/form/section.vue'; +import * as os from '@/os.js'; +import { fetchInstance } from '@/instance.js'; +import { i18n } from '@/i18n.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; + +let deeplAuthKey: string = $ref(''); +let deeplIsPro: boolean = $ref(false); + +async function init() { + const meta = await os.api('admin/meta'); + deeplAuthKey = meta.deeplAuthKey; + deeplIsPro = meta.deeplIsPro; +} + +function save() { + os.apiWithDialog('admin/update-meta', { + deeplAuthKey, + deeplIsPro, + }).then(() => { + fetchInstance(); + }); +} + +const headerActions = $computed(() => []); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.externalServices, + icon: 'ti ti-link', +}); +</script> + +<style lang="scss" module> +.footer { + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); +} +</style> diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 944ba7b950e1f4840d1d08debc68af7518d302e6..a508c20cf317c0a2d82e16756d2ff85852c8695b 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -198,6 +198,11 @@ const menuDef = $computed(() => [{ text: i18n.ts.proxyAccount, to: '/admin/proxy-account', active: currentPage?.route.name === 'proxy-account', + }, { + icon: 'ti ti-link', + text: i18n.ts.externalServices, + to: '/admin/external-services', + active: currentPage?.route.name === 'external-services', }, { icon: 'ti ti-adjustments', text: i18n.ts.other, diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 46f92729e8168f67bd6ad970a483d3e2826fb77e..8b160635f71f03b012326f28643b0a00055d44d3 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -25,6 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.tosUrl }}</template> </MkInput> + <MkInput v-model="privacyPolicyUrl"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.privacyPolicyUrl }}</template> + </MkInput> + <MkTextarea v-model="preservedUsernames"> <template #label>{{ i18n.ts.preservedUsernames }}</template> <template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template> @@ -69,6 +74,7 @@ let emailRequiredForSignup: boolean = $ref(false); let sensitiveWords: string = $ref(''); let preservedUsernames: string = $ref(''); let tosUrl: string | null = $ref(null); +let privacyPolicyUrl: string | null = $ref(null); async function init() { const meta = await os.api('admin/meta'); @@ -77,6 +83,7 @@ async function init() { sensitiveWords = meta.sensitiveWords.join('\n'); preservedUsernames = meta.preservedUsernames.join('\n'); tosUrl = meta.tosUrl; + privacyPolicyUrl = meta.privacyPolicyUrl; } function save() { @@ -84,6 +91,7 @@ function save() { disableRegistration: !enableRegistration, emailRequiredForSignup, tosUrl, + privacyPolicyUrl, sensitiveWords: sensitiveWords.split('\n'), preservedUsernames: preservedUsernames.split('\n'), }).then(() => { diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index 66561c969e5799b2a9b7c9ef42a8a43cf53db162..0af226f02e3ffb8b9d8f3a5c29ff0f144410efc6 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -29,8 +29,12 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span> <span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span> <span v-else-if="log.type === 'unsuspendRemoteInstance'">: {{ log.info.host }}</span> + <span v-else-if="log.type === 'createGlobalAnnouncement'">: {{ log.info.announcement.title }}</span> + <span v-else-if="log.type === 'updateGlobalAnnouncement'">: {{ log.info.before.title }}</span> + <span v-else-if="log.type === 'deleteGlobalAnnouncement'">: {{ log.info.announcement.title }}</span> <span v-else-if="log.type === 'createUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> <span v-else-if="log.type === 'updateUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> + <span v-else-if="log.type === 'deleteUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> <span v-else-if="log.type === 'deleteNote'">: @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }}</span> <span v-else-if="log.type === 'deleteDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span> </template> @@ -88,6 +92,16 @@ SPDX-License-Identifier: AGPL-3.0-only <CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/> </div> </template> + <template v-else-if="log.type === 'updateGlobalAnnouncement'"> + <div :class="$style.diff"> + <CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/> + </div> + </template> + <template v-else-if="log.type === 'updateUserAnnouncement'"> + <div :class="$style.diff"> + <CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/> + </div> + </template> <details> <summary>raw</summary> diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 8015bb7a7f7393017cbad936ce4d7a0bb54534ad..ead2250af2c384e2fa387f14d2b82936e58e0936 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -160,26 +160,6 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.canEditNote, 'canEditNote'])"> - <template #label>{{ i18n.ts._role._options.canEditNote }}</template> - <template #suffix> - <span v-if="role.policies.canEditNote.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> - <span v-else>{{ role.policies.canEditNote.value ? i18n.ts.yes : i18n.ts.no }}</span> - <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canEditNote)"></i></span> - </template> - <div class="_gaps"> - <MkSwitch v-model="role.policies.canEditNote.useDefault" :readonly="readonly"> - <template #label>{{ i18n.ts._role.useBaseValue }}</template> - </MkSwitch> - <MkSwitch v-model="role.policies.canEditNote.value" :disabled="role.policies.canEditNote.useDefault" :readonly="readonly"> - <template #label>{{ i18n.ts.enable }}</template> - </MkSwitch> - <MkRange v-model="role.policies.canEditNote.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> - <template #label>{{ i18n.ts._role.priority }}</template> - </MkRange> - </div> - </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])"> <template #label>{{ i18n.ts._role._options.canInvite }}</template> <template #suffix> diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 001cf3490e9f4968551dafb66bf2b18074773a22..74de9f73966f627791e54a23cb864931161e6df1 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -48,14 +48,6 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSwitch> </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.canEditNote, 'canEditNote'])"> - <template #label>{{ i18n.ts._role._options.canEditNote }}</template> - <template #suffix>{{ policies.canEditNote ? i18n.ts.yes : i18n.ts.no }}</template> - <MkSwitch v-model="policies.canEditNote"> - <template #label>{{ i18n.ts.enable }}</template> - </MkSwitch> - </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])"> <template #label>{{ i18n.ts._role._options.canInvite }}</template> <template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template> diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index f93678d7280215fe43860287abfe1c39681d0929..0072d666c902b3659635067fe43cec58a2c20e81 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -34,6 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> </FormSplit> + <MkInput v-model="impressumUrl"> + <template #label>{{ i18n.ts.impressumUrl }}</template> + <template #prefix><i class="ti ti-link"></i></template> + <template #caption>{{ i18n.ts.impressumDescription }}</template> + </MkInput> + <MkTextarea v-model="pinnedUsers"> <template #label>{{ i18n.ts.pinnedUsers }}</template> <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template> @@ -81,16 +87,40 @@ SPDX-License-Identifier: AGPL-3.0-only </FormSection> <FormSection> - <template #label>DeepL Translation</template> + <template #label>Timeline caching</template> <div class="_gaps_m"> - <MkInput v-model="deeplAuthKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>DeepL Auth Key</template> + <MkInput v-model="perLocalUserUserTimelineCacheMax" type="number"> + <template #label>perLocalUserUserTimelineCacheMax</template> + </MkInput> + + <MkInput v-model="perRemoteUserUserTimelineCacheMax" type="number"> + <template #label>perRemoteUserUserTimelineCacheMax</template> + </MkInput> + + <MkInput v-model="perUserHomeTimelineCacheMax" type="number"> + <template #label>perUserHomeTimelineCacheMax</template> + </MkInput> + + <MkInput v-model="perUserListTimelineCacheMax" type="number"> + <template #label>perUserListTimelineCacheMax</template> </MkInput> - <MkSwitch v-model="deeplIsPro"> - <template #label>Pro account</template> - </MkSwitch> + </div> + </FormSection> + + <FormSection> + <template #label>{{ i18n.ts._ad.adsSettings }}</template> + + <div class="_gaps_m"> + <div class="_gaps_s"> + <MkInput v-model="notesPerOneAd" :min="0" type="number"> + <template #label>{{ i18n.ts._ad.notesPerOneAd }}</template> + <template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template> + </MkInput> + <MkInfo v-if="notesPerOneAd > 0 && notesPerOneAd < 20" :warn="true"> + {{ i18n.ts._ad.adsTooClose }} + </MkInfo> + </div> </div> </FormSection> </div> @@ -113,6 +143,7 @@ import XHeader from './_header_.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; +import MkInfo from '@/components/MkInfo.vue'; import FormSection from '@/components/form/section.vue'; import FormSplit from '@/components/form/split.vue'; import FormSuspense from '@/components/form/suspense.vue'; @@ -127,14 +158,18 @@ let shortName: string | null = $ref(null); let description: string | null = $ref(null); let maintainerName: string | null = $ref(null); let maintainerEmail: string | null = $ref(null); +let impressumUrl: string | null = $ref(null); let pinnedUsers: string = $ref(''); let cacheRemoteFiles: boolean = $ref(false); let cacheRemoteSensitiveFiles: boolean = $ref(false); let enableServiceWorker: boolean = $ref(false); let swPublicKey: any = $ref(null); let swPrivateKey: any = $ref(null); -let deeplAuthKey: string = $ref(''); -let deeplIsPro: boolean = $ref(false); +let perLocalUserUserTimelineCacheMax: number = $ref(0); +let perRemoteUserUserTimelineCacheMax: number = $ref(0); +let perUserHomeTimelineCacheMax: number = $ref(0); +let perUserListTimelineCacheMax: number = $ref(0); +let notesPerOneAd: number = $ref(0); async function init(): Promise<void> { const meta = await os.api('admin/meta'); @@ -143,34 +178,42 @@ async function init(): Promise<void> { description = meta.description; maintainerName = meta.maintainerName; maintainerEmail = meta.maintainerEmail; + impressumUrl = meta.impressumUrl; pinnedUsers = meta.pinnedUsers.join('\n'); cacheRemoteFiles = meta.cacheRemoteFiles; cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles; enableServiceWorker = meta.enableServiceWorker; swPublicKey = meta.swPublickey; swPrivateKey = meta.swPrivateKey; - deeplAuthKey = meta.deeplAuthKey; - deeplIsPro = meta.deeplIsPro; + perLocalUserUserTimelineCacheMax = meta.perLocalUserUserTimelineCacheMax; + perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax; + perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax; + perUserListTimelineCacheMax = meta.perUserListTimelineCacheMax; + notesPerOneAd = meta.notesPerOneAd; } -function save(): void { - os.apiWithDialog('admin/update-meta', { +async function save(): void { + await os.apiWithDialog('admin/update-meta', { name, shortName: shortName === '' ? null : shortName, description, maintainerName, maintainerEmail, + impressumUrl, pinnedUsers: pinnedUsers.split('\n'), cacheRemoteFiles, cacheRemoteSensitiveFiles, enableServiceWorker, swPublicKey, swPrivateKey, - deeplAuthKey, - deeplIsPro, - }).then(() => { - fetchInstance(); + perLocalUserUserTimelineCacheMax, + perRemoteUserUserTimelineCacheMax, + perUserHomeTimelineCacheMax, + perUserListTimelineCacheMax, + notesPerOneAd, }); + + fetchInstance(); } const headerTabs = $computed(() => []); diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 00e88cbbfb71334d3947f8013221f9b3c8a1a46c..911f4e95d2e28d0542af7868484c49fad32e79c7 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -102,7 +102,6 @@ let searchKey = $ref(''); const featuredPagination = $computed(() => ({ endpoint: 'notes/featured' as const, limit: 10, - offsetMode: true, params: { channelId: props.channelId, }, diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue new file mode 100644 index 0000000000000000000000000000000000000000..ae9256b8e3141dca997da29a2b7082b1e5ea14c2 --- /dev/null +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -0,0 +1,302 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div class="_gaps"> + <MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo> + <MkLoading v-if="fetching"/> + <div v-else-if="file" class="_gaps"> + <div :class="$style.filePreviewRoot"> + <MkMediaList :mediaList="[file]"></MkMediaList> + </div> + <div :class="$style.fileQuickActionsRoot"> + <button class="_button" :class="$style.fileNameEditBtn" @click="rename()"> + <h2 class="_nowrap" :class="$style.fileName">{{ file.name }}</h2> + <i class="ti ti-pencil" :class="$style.fileNameEditIcon"></i> + </button> + <div :class="$style.fileQuickActionsOthers"> + <button v-tooltip="i18n.ts.createNoteFromTheFile" class="_button" :class="$style.fileQuickActionsOthersButton" @click="postThis()"> + <i class="ti ti-pencil"></i> + </button> + <button v-if="isImage" v-tooltip="i18n.ts.cropImage" class="_button" :class="$style.fileQuickActionsOthersButton" @click="crop()"> + <i class="ti ti-crop"></i> + </button> + <button v-if="file.isSensitive" v-tooltip="i18n.ts.unmarkAsSensitive" class="_button" :class="$style.fileQuickActionsOthersButton" @click="toggleSensitive()"> + <i class="ti ti-eye"></i> + </button> + <button v-else v-tooltip="i18n.ts.markAsSensitive" class="_button" :class="$style.fileQuickActionsOthersButton" @click="toggleSensitive()"> + <i class="ti ti-eye-exclamation"></i> + </button> + <a v-tooltip="i18n.ts.download" :href="file.url" :download="file.name" class="_button" :class="$style.fileQuickActionsOthersButton"> + <i class="ti ti-download"></i> + </a> + <button v-tooltip="i18n.ts.delete" class="_button" :class="[$style.fileQuickActionsOthersButton, $style.danger]" @click="deleteFile()"> + <i class="ti ti-trash"></i> + </button> + </div> + </div> + <div> + <button class="_button" :class="$style.fileAltEditBtn" @click="describe()"> + <MkKeyValue> + <template #key>{{ i18n.ts.description }}</template> + <template #value>{{ file.comment ? file.comment : `(${i18n.ts.none})` }}<i class="ti ti-pencil" :class="$style.fileAltEditIcon"></i></template> + </MkKeyValue> + </button> + <MkKeyValue :class="$style.fileMetaDataChildren"> + <template #key>{{ i18n.ts._fileViewer.uploadedAt }}</template> + <template #value><MkTime :time="file.createdAt" mode="detail"/></template> + </MkKeyValue> + <MkKeyValue :class="$style.fileMetaDataChildren"> + <template #key>{{ i18n.ts._fileViewer.type }}</template> + <template #value>{{ file.type }}</template> + </MkKeyValue> + <MkKeyValue :class="$style.fileMetaDataChildren"> + <template #key>{{ i18n.ts._fileViewer.size }}</template> + <template #value>{{ bytes(file.size) }}</template> + </MkKeyValue> + </div> + </div> + <div v-else class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.nothing }}</div> + </div> +</div> +</template> + +<script setup lang="ts"> +import { ref, computed, defineAsyncComponent, onMounted } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkInfo from '@/components/MkInfo.vue'; +import MkMediaList from '@/components/MkMediaList.vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; +import bytes from '@/filters/bytes.js'; +import { infoImageUrl } from '@/instance.js'; +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; +import { useRouter } from '@/router.js'; + +const router = useRouter(); + +const props = defineProps<{ + fileId: string; +}>(); + +const fetching = ref(true); +const file = ref<Misskey.entities.DriveFile>(); +const isImage = computed(() => file.value?.type.startsWith('image/')); + +async function fetch() { + fetching.value = true; + + file.value = await os.api('drive/files/show', { + fileId: props.fileId, + }).catch((err) => { + console.error(err); + return undefined; + }); + + fetching.value = false; +} + +function postThis() { + if (!file.value) return; + + os.post({ + initialFiles: [file.value], + }); +} + +function crop() { + if (!file.value) return; + + os.cropImage(file.value, { + aspectRatio: NaN, + uploadFolder: file.value.folderId ?? null, + }); +} + +function toggleSensitive() { + if (!file.value) return; + + os.apiWithDialog('drive/files/update', { + fileId: file.value.id, + isSensitive: !file.value.isSensitive, + }).then(async () => { + await fetch(); + }).catch(err => { + os.alert({ + type: 'error', + title: i18n.ts.error, + text: err.message, + }); + }); +} + +function rename() { + if (!file.value) return; + + os.inputText({ + title: i18n.ts.renameFile, + placeholder: i18n.ts.inputNewFileName, + default: file.value.name, + }).then(({ canceled, result: name }) => { + if (canceled) return; + os.apiWithDialog('drive/files/update', { + fileId: file.value.id, + name: name, + }).then(async () => { + await fetch(); + }); + }); +} + +function describe() { + if (!file.value) return; + + os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { + default: file.value.comment ?? '', + file: file.value, + }, { + done: caption => { + os.apiWithDialog('drive/files/update', { + fileId: file.value.id, + comment: caption.length === 0 ? null : caption, + }).then(async () => { + await fetch(); + }); + }, + }, 'closed'); +} + +async function deleteFile() { + if (!file.value) return; + + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('driveFileDeleteConfirm', { name: file.value.name }), + }); + + if (canceled) return; + await os.apiWithDialog('drive/files/delete', { + fileId: file.value.id, + }); + + router.push('/my/drive'); +} + +onMounted(async () => { + await fetch(); +}); +</script> + +<style lang="scss" module> + +.filePreviewRoot { + background: var(--panel); + border-radius: var(--radius); + // MkMediaList 内ã®ä¸Šéƒ¨ãƒžãƒ¼ã‚¸ãƒ³ 4px + padding: calc(1rem - 4px) 1rem 1rem; +} + +.fileQuickActionsRoot { + display: flex; + flex-direction: column; + gap: 8px; +} + +@container (min-width: 500px) { + .fileQuickActionsRoot { + flex-direction: row; + align-items: center; + } +} + +.fileQuickActionsOthers { + margin-left: auto; + margin-right: 1rem; + display: flex; + gap: 8px; + + .fileQuickActionsOthersButton { + padding: .5rem; + border-radius: 99rem; + + &:hover, + &:focus-visible { + background-color: var(--accentedBg); + color: var(--accent); + text-decoration: none; + } + + &.danger { + color: #ff2a2a; + } + + &.danger:hover, + &.danger:focus-visible { + background-color: rgba(255, 42, 42, .15); + } + } +} + +.fileNameEditBtn { + padding: .5rem 1rem; + display: flex; + align-items: center; + min-width: 0; + font-weight: 700; + border-radius: var(--radius); + font-size: .8rem; + + >.fileNameEditIcon { + color: transparent; + visibility: hidden; + padding-left: .5rem; + } + + >.fileName { + margin: 0; + } + + &:hover { + background-color: var(--accentedBg); + + >.fileName, + >.fileNameEditIcon { + visibility: visible; + color: var(--accent); + } + } +} + +.fileMetaDataChildren { + padding: .5rem 1rem; +} + +.fileAltEditBtn { + text-align: start; + display: block; + width: 100%; + padding: .5rem 1rem; + border-radius: var(--radius); + + .fileAltEditIcon { + display: inline-block; + color: transparent; + visibility: hidden; + padding-left: .5rem; + } + + &:hover { + color: var(--accent); + background-color: var(--accentedBg); + + .fileAltEditIcon { + color: var(--accent); + visibility: visible; + } + } +} +</style> diff --git a/packages/frontend/src/pages/drive.file.notes.vue b/packages/frontend/src/pages/drive.file.notes.vue new file mode 100644 index 0000000000000000000000000000000000000000..ee1a0ee9b0e5fc96867c15d97da94ed9396da9d4 --- /dev/null +++ b/packages/frontend/src/pages/drive.file.notes.vue @@ -0,0 +1,33 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div class="_gaps"> + <MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo> + <MkNotes ref="tlComponent" :pagination="pagination"/> +</div> +</template> + +<script lang="ts" setup> +import { ref, computed } from 'vue'; +import { i18n } from '@/i18n.js'; +import { Paging } from '@/components/MkPagination.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import MkNotes from '@/components/MkNotes.vue'; + +const props = defineProps<{ + fileId: string; +}>(); + +const realFileId = computed(() => props.fileId); + +const pagination = ref<Paging>({ + endpoint: 'drive/files/attached-notes', + limit: 10, + params: { + fileId: realFileId.value, + }, +}); +</script> diff --git a/packages/frontend/src/pages/drive.file.vue b/packages/frontend/src/pages/drive.file.vue new file mode 100644 index 0000000000000000000000000000000000000000..2c1e5d20a77e123df7d0bd2be94bc1f979f78d9a --- /dev/null +++ b/packages/frontend/src/pages/drive.file.vue @@ -0,0 +1,52 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkStickyContainer> + <template #header> + <MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/> + </template> + + <MkSpacer v-if="tab === 'info'" :contentMax="800"> + <XFileInfo :fileId="fileId"/> + </MkSpacer> + + <MkSpacer v-else-if="tab === 'notes'" :contentMax="800"> + <XNotes :fileId="fileId"/> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { computed, ref, defineAsyncComponent } from 'vue'; +import { i18n } from '@/i18n.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; + +const props = defineProps<{ + fileId: string; +}>(); + +const XFileInfo = defineAsyncComponent(() => import('./drive.file.info.vue')); +const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue')); + +const tab = ref('info'); + +const headerActions = computed(() => []); + +const headerTabs = computed(() => [{ + key: 'info', + title: i18n.ts.info, + icon: 'ti ti-info-circle', +}, { + key: 'notes', + title: i18n.ts._fileViewer.attachedNotes, + icon: 'ti ti-pencil', +}]); + +definePageMetadata(computed(() => ({ + title: i18n.ts._fileViewer.title, + icon: 'ti ti-file', +}))); +</script> diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue index 0558faec1629475c18528f59aa8a44b530a758be..a36d1b3bda101dbabbe72bd00e84d0385aeb0411 100644 --- a/packages/frontend/src/pages/explore.featured.vue +++ b/packages/frontend/src/pages/explore.featured.vue @@ -22,7 +22,6 @@ import { i18n } from '@/i18n.js'; const paginationForNotes = { endpoint: 'notes/featured' as const, limit: 10, - offsetMode: true, }; const paginationForPolls = { diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index df13998f6be46c7710c8c31e9f5d90b5c42761c1..b600f99fbcd02e4f0a09af20ed1d9643f543f0da 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -29,16 +29,22 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_s"> <MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton> - <div v-for="user in users" :key="user.id" :class="$style.userItem"> - <MkA :class="$style.userItemBody" :to="`${userPage(user)}`"> - <MkUserCardMini :user="user"/> - </MkA> - <button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button> - </div> - <MkButton v-if="!fetching && queueUserIds.length !== 0" v-appear="enableInfiniteScroll ? fetchMoreUsers : null" :class="$style.more" :style="{ cursor: 'pointer' }" primary rounded @click="fetchMoreUsers"> - {{ i18n.ts.loadMore }} - </MkButton> - <MkLoading v-if="fetching" class="loading"/> + + <MkPagination ref="paginationEl" :pagination="membershipsPagination"> + <template #default="{ items }"> + <div class="_gaps_s"> + <div v-for="item in items" :key="item.id"> + <div :class="$style.userItem"> + <MkA :class="$style.userItemBody" :to="`${userPage(item.user)}`"> + <MkUserCardMini :user="item.user"/> + </MkA> + <button class="_button" :class="$style.menu" @click="showMembershipMenu(item, $event)"><i class="ti ti-dots"></i></button> + <button class="_button" :class="$style.remove" @click="removeUser(item, $event)"><i class="ti ti-x"></i></button> + </div> + </div> + </div> + </template> + </MkPagination> </div> </MkFolder> </div> @@ -59,9 +65,11 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkInput from '@/components/MkInput.vue'; -import { userListsCache } from '@/cache'; +import { userListsCache } from '@/cache.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; +import MkPagination, { Paging } from '@/components/MkPagination.vue'; + const { enableInfiniteScroll, } = defaultStore.reactiveState; @@ -70,40 +78,25 @@ const props = defineProps<{ listId: string; }>(); -const FETCH_USERS_LIMIT = 20; - +const paginationEl = ref<InstanceType<typeof MkPagination>>(); let list = $ref<Misskey.entities.UserList | null>(null); -let users = $ref<Misskey.entities.UserLite[]>([]); -let queueUserIds = $ref<string[]>([]); -let fetching = $ref(true); const isPublic = ref(false); const name = ref(''); +const membershipsPagination = { + endpoint: 'users/lists/get-memberships' as const, + limit: 30, + params: computed(() => ({ + listId: props.listId, + })), +}; function fetchList() { - fetching = true; os.api('users/lists/show', { listId: props.listId, }).then(_list => { list = _list; name.value = list.name; isPublic.value = list.isPublic; - queueUserIds = list.userIds; - - return fetchMoreUsers(); - }); -} - -function fetchMoreUsers() { - if (!list) return; - if (fetching && users.length !== 0) return; // fetchingãŒtrueãªã‚‰ã‚„ã‚ã‚‹ãŒã€usersãŒç©ºãªã‚‰ç¶šè¡Œ - fetching = true; - os.api('users/show', { - userIds: queueUserIds.slice(0, FETCH_USERS_LIMIT), - }).then(_users => { - users = users.concat(_users); - queueUserIds = queueUserIds.slice(FETCH_USERS_LIMIT); - }).finally(() => { - fetching = false; }); } @@ -114,12 +107,12 @@ function addUser() { listId: list.id, userId: user.id, }).then(() => { - users.push(user); + paginationEl.value.reload(); }); }); } -async function removeUser(user, ev) { +async function removeUser(item, ev) { os.popupMenu([{ text: i18n.ts.remove, icon: 'ti ti-x', @@ -128,9 +121,28 @@ async function removeUser(user, ev) { if (!list) return; os.api('users/lists/pull', { listId: list.id, - userId: user.id, + userId: item.userId, + }).then(() => { + paginationEl.value.removeItem(item.id); + }); + }, + }], ev.currentTarget ?? ev.target); +} + +async function showMembershipMenu(item, ev) { + os.popupMenu([{ + text: item.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline, + icon: item.withReplies ? 'ti ti-messages-off' : 'ti ti-messages', + action: async () => { + os.api('users/lists/update-membership', { + listId: list.id, + userId: item.userId, + withReplies: !item.withReplies, }).then(() => { - users = users.filter(x => x.id !== user.id); + paginationEl.value.updateItem(item.id, (old) => ({ + ...old, + withReplies: !item.withReplies, + })); }); }, }], ev.currentTarget ?? ev.target); @@ -202,6 +214,12 @@ definePageMetadata(computed(() => list ? { align-self: center; } +.menu { + width: 32px; + height: 32px; + align-self: center; +} + .more { margin-left: auto; margin-right: auto; diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index 377267bdbcbeecb181cca0a133c5908c83fd626f..dc749c292eb1b5be572315031d81d3b32da6b055 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -286,8 +286,7 @@ definePageMetadata(computed(() => { let title = i18n.ts._pages.newPage; if (props.initPageId) { title = i18n.ts._pages.editPage; - } - else if (props.initPageName && props.initUser) { + } else if (props.initPageName && props.initUser) { title = i18n.ts._pages.readPage; } return { diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue index c2d1694c5b2442604bd7214ed437274858b351dd..4641b4910321401738bff734716a4e4ac89b8d94 100644 --- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue +++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue @@ -83,6 +83,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #value><code class="_monospace">{{ code }}</code></template> </MkKeyValue> </div> + + <MkButton primary rounded gradate @click="downloadBackupCodes"><i class="ti ti-download"></i> {{ i18n.ts.download }}</MkButton> </div> </MkFolder> </div> @@ -108,6 +110,7 @@ import * as os from '@/os.js'; import MkFolder from '@/components/MkFolder.vue'; import MkInfo from '@/components/MkInfo.vue'; import { confetti } from '@/scripts/confetti.js'; +import { $i } from '@/account.js'; defineProps<{ twoFactorData: { @@ -143,6 +146,16 @@ async function tokenDone() { }); } +function downloadBackupCodes() { + if (backupCodes.value !== undefined) { + const txtBlob = new Blob([backupCodes.value.join('\n')], { type: 'text/plain' }); + const dummya = document.createElement('a'); + dummya.href = URL.createObjectURL(txtBlob); + dummya.download = `${$i?.username}-2fa-backup-codes.txt`; + dummya.click(); + } +} + function allDone() { dialog.value.close(); } diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index f1bd501150747dd3e10526b80006ad73556fa703..cfabbbbf65239a0c76d39364fb7a3764c8d9be57 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -139,21 +139,11 @@ const menuDef = computed(() => [{ text: i18n.ts.roles, to: '/settings/roles', active: currentPage?.route.name === 'roles', - }, { - icon: 'ti ti-planet-off', - text: i18n.ts.instanceMute, - to: '/settings/instance-mute', - active: currentPage?.route.name === 'instance-mute', }, { icon: 'ti ti-ban', text: i18n.ts.muteAndBlock, to: '/settings/mute-block', active: currentPage?.route.name === 'mute-block', - }, { - icon: 'ti ti-message-off', - text: i18n.ts.wordMute, - to: '/settings/word-mute', - active: currentPage?.route.name === 'word-mute', }, { icon: 'ti ti-api', text: 'API', diff --git a/packages/frontend/src/pages/settings/instance-mute.vue b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue similarity index 85% rename from packages/frontend/src/pages/settings/instance-mute.vue rename to packages/frontend/src/pages/settings/mute-block.instance-mute.vue index b76fd2c90636127bb38cb4e2740a65ddefbed090..4b5080ea8fa057f7b9a0fe57fd9f1e3f9d90e160 100644 --- a/packages/frontend/src/pages/settings/instance-mute.vue +++ b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue @@ -22,7 +22,6 @@ import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; const instanceMutes = ref($i!.mutedInstances.join('\n')); const changed = ref(false); @@ -46,13 +45,4 @@ async function save() { watch(instanceMutes, () => { changed.value = true; }); - -const headerActions = $computed(() => []); - -const headerTabs = $computed(() => []); - -definePageMetadata({ - title: i18n.ts.instanceMute, - icon: 'ti ti-planet-off', -}); </script> diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index 37d3d1773fb00c3737d2c21136099ffe71794d6a..c6cbd424ec0f37e1251fd18f54dc759dbb18c08c 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -5,13 +5,24 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> - <MkTab v-model="tab"> - <option value="renoteMute">{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</option> - <option value="mute">{{ i18n.ts.mutedUsers }}</option> - <option value="block">{{ i18n.ts.blockedUsers }}</option> - </MkTab> + <MkFolder> + <template #icon><i class="ti ti-message-off"></i></template> + <template #label>{{ i18n.ts.wordMute }}</template> + + <XWordMute/> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-planet-off"></i></template> + <template #label>{{ i18n.ts.instanceMute }}</template> + + <XInstanceMute/> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-repeat-off"></i></template> + <template #label>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</template> - <div v-if="tab === 'renoteMute'"> <MkPagination :pagination="renoteMutingPagination"> <template #empty> <div class="_fullinfo"> @@ -37,9 +48,12 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> </MkPagination> - </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-eye-off"></i></template> + <template #label>{{ i18n.ts.mutedUsers }}</template> - <div v-else-if="tab === 'mute'"> <MkPagination :pagination="mutingPagination"> <template #empty> <div class="_fullinfo"> @@ -67,9 +81,12 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> </MkPagination> - </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-ban"></i></template> + <template #label>{{ i18n.ts.blockedUsers }}</template> - <div v-else-if="tab === 'block'"> <MkPagination :pagination="blockingPagination"> <template #empty> <div class="_fullinfo"> @@ -97,24 +114,22 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> </MkPagination> - </div> + </MkFolder> </div> </template> <script lang="ts" setup> import { } from 'vue'; +import XInstanceMute from './mute-block.instance-mute.vue'; +import XWordMute from './mute-block.word-mute.vue'; import MkPagination from '@/components/MkPagination.vue'; -import MkTab from '@/components/MkTab.vue'; -import FormInfo from '@/components/MkInfo.vue'; -import FormLink from '@/components/form/link.vue'; import { userPage } from '@/filters/user.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import * as os from '@/os.js'; import { infoImageUrl } from '@/instance.js'; - -let tab = $ref('renoteMute'); +import MkFolder from '@/components/MkFolder.vue'; const renoteMutingPagination = { endpoint: 'renote-mute/list' as const, diff --git a/packages/frontend/src/pages/settings/word-mute.vue b/packages/frontend/src/pages/settings/mute-block.word-mute.vue similarity index 51% rename from packages/frontend/src/pages/settings/word-mute.vue rename to packages/frontend/src/pages/settings/mute-block.word-mute.vue index 1fefbdc92ba131cc936bec974fda61e988f0ca68..25a836ea55e445579463bd39b700dfe2bb96e1d7 100644 --- a/packages/frontend/src/pages/settings/word-mute.vue +++ b/packages/frontend/src/pages/settings/mute-block.word-mute.vue @@ -5,29 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> - <MkTab v-model="tab"> - <option value="soft">{{ i18n.ts._wordMute.soft }}</option> - <option value="hard">{{ i18n.ts._wordMute.hard }}</option> - </MkTab> <div> - <div v-show="tab === 'soft'" class="_gaps_m"> - <MkInfo>{{ i18n.ts._wordMute.softDescription }}</MkInfo> - <MkTextarea v-model="softMutedWords"> - <span>{{ i18n.ts._wordMute.muteWords }}</span> - <template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template> - </MkTextarea> - </div> - <div v-show="tab === 'hard'" class="_gaps_m"> - <MkInfo>{{ i18n.ts._wordMute.hardDescription }} {{ i18n.ts.reflectMayTakeTime }}</MkInfo> - <MkTextarea v-model="hardMutedWords"> - <span>{{ i18n.ts._wordMute.muteWords }}</span> - <template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template> - </MkTextarea> - <MkKeyValue v-if="hardWordMutedNotesCount != null"> - <template #key>{{ i18n.ts._wordMute.mutedNotes }}</template> - <template #value>{{ number(hardWordMutedNotesCount) }}</template> - </MkKeyValue> - </div> + <MkTextarea v-model="mutedWords"> + <span>{{ i18n.ts._wordMute.muteWords }}</span> + <template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template> + </MkTextarea> </div> <MkButton primary inline :disabled="!changed" @click="save()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> @@ -56,25 +38,15 @@ const render = (mutedWords) => mutedWords.map(x => { }).join('\n'); const tab = ref('soft'); -const softMutedWords = ref(render(defaultStore.state.mutedWords)); -const hardMutedWords = ref(render($i!.mutedWords)); -const hardWordMutedNotesCount = ref(null); +const mutedWords = ref(render($i!.mutedWords)); const changed = ref(false); -os.api('i/get-word-muted-notes-count', {}).then(response => { - hardWordMutedNotesCount.value = response?.count; -}); - -watch(softMutedWords, () => { - changed.value = true; -}); - -watch(hardMutedWords, () => { +watch(mutedWords, () => { changed.value = true; }); async function save() { - const parseMutes = (mutes, tab) => { + const parseMutes = (mutes) => { // split into lines, remove empty lines and unnecessary whitespace let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== ''); @@ -92,7 +64,7 @@ async function save() { os.alert({ type: 'error', title: i18n.ts.regexpError, - text: i18n.t('regexpErrorDescription', { tab, line: i + 1 }) + '\n' + err.toString(), + text: i18n.t('regexpErrorDescription', { tab: 'word mute', line: i + 1 }) + '\n' + err.toString(), }); // re-throw error so these invalid settings are not saved throw err; @@ -105,29 +77,18 @@ async function save() { return lines; }; - let softMutes, hardMutes; + let parsed; try { - softMutes = parseMutes(softMutedWords.value, i18n.ts._wordMute.soft); - hardMutes = parseMutes(hardMutedWords.value, i18n.ts._wordMute.hard); + parsed = parseMutes(mutedWords.value); } catch (err) { // already displayed error message in parseMutes return; } - defaultStore.set('mutedWords', softMutes); await os.api('i/update', { - mutedWords: hardMutes, + mutedWords: parsed, }); changed.value = false; } - -const headerActions = $computed(() => []); - -const headerTabs = $computed(() => []); - -definePageMetadata({ - title: i18n.ts.wordMute, - icon: 'ti ti-message-off', -}); </script> diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue index b6ac17ed4f9f5f8219cdf1dcb49c7a11b35a0bff..819e7ffe53ae16debbc026df219848afd8393a87 100644 --- a/packages/frontend/src/pages/settings/sounds.vue +++ b/packages/frontend/src/pages/settings/sounds.vue @@ -38,14 +38,12 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; const masterVolume = computed(soundConfigStore.makeGetterSetter('sound_masterVolume')); -const soundsKeys = ['note', 'noteMy', 'notification', 'chat', 'chatBg', 'antenna', 'channel'] as const; +const soundsKeys = ['note', 'noteMy', 'notification', '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, }); diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 5bad689aee250c02b89ab6a699aeab8473b32bc3..b8deb779524146995c79624ecc1e0bcd88f3a685 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -15,11 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.tl"> <MkTimeline ref="tlComponent" - :key="src + withRenotes + withReplies + onlyFiles" + :key="src + withRenotes + onlyFiles" :src="src.split(':')[0]" :list="src.split(':')[1]" :withRenotes="withRenotes" - :withReplies="withReplies" :onlyFiles="onlyFiles" :sound="true" @queue="queueUpdated" @@ -62,7 +61,6 @@ let queue = $ref(0); let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global'); const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) }); const withRenotes = $ref(true); -const withReplies = $ref(false); const onlyFiles = $ref(false); watch($$(src), () => queue = 0); @@ -144,11 +142,6 @@ const headerActions = $computed(() => [{ text: i18n.ts.showRenotes, icon: 'ti ti-repeat', ref: $$(withRenotes), - }, { - type: 'switch', - text: i18n.ts.withReplies, - icon: 'ti ti-arrow-back-up', - ref: $$(withReplies), }, { type: 'switch', text: i18n.ts.fileAttachedOnly, diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index 83244876fe795f0a0fb6e345a8ff11da5e842027..0fc7b62d827270a55f18a3dfde8907d30cbe7173 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -61,20 +61,7 @@ function settings() { router.push(`/my/lists/${props.listId}`); } -async function timetravel() { - const { canceled, result: date } = await os.inputDate({ - title: i18n.ts.date, - }); - if (canceled) return; - - tlEl.timetravel(date); -} - const headerActions = $computed(() => list ? [{ - icon: 'ti ti-calendar-time', - text: i18n.ts.jumpToSpecifiedDate, - handler: timetravel, -}, { icon: 'ti ti-settings', text: i18n.ts.settings, handler: settings, diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 385c81a97ffbcce6a5e06ba1cee4c8ba51af5c38..605e9fbb76390c904c1668a5c36e558ebb1bfc2d 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -128,14 +128,17 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo> <template v-if="narrow"> - <XPhotos :key="user.id" :user="user"/> + <XFiles :key="user.id" :user="user"/> <XActivity :key="user.id" :user="user"/> </template> - <MkNotes v-if="!disableNotes" :class="$style.tl" :noGap="true" :pagination="pagination"/> + <div v-if="!disableNotes"> + <div style="margin-bottom: 8px;">{{ i18n.ts.featured }}</div> + <MkNotes :class="$style.tl" :noGap="true" :pagination="pagination"/> + </div> </div> </div> <div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;"> - <XPhotos :key="user.id" :user="user"/> + <XFiles :key="user.id" :user="user"/> <XActivity :key="user.id" :user="user"/> </div> </div> @@ -182,7 +185,7 @@ function calcAge(birthdate: string): number { return yearDiff; } -const XPhotos = defineAsyncComponent(() => import('./index.photos.vue')); +const XFiles = defineAsyncComponent(() => import('./index.files.vue')); const XActivity = defineAsyncComponent(() => import('./index.activity.vue')); const props = withDefaults(defineProps<{ @@ -210,7 +213,7 @@ watch($$(moderationNote), async () => { }); const pagination = { - endpoint: 'users/notes' as const, + endpoint: 'users/featured-notes' as const, limit: 10, params: computed(() => ({ userId: props.user.id, diff --git a/packages/frontend/src/pages/user/index.photos.vue b/packages/frontend/src/pages/user/index.files.vue similarity index 63% rename from packages/frontend/src/pages/user/index.photos.vue rename to packages/frontend/src/pages/user/index.files.vue index b6cae9f13109a86fc2982856ee261acf079bda02..43d6d91fc9703f556fb11a62f1af996a4e97c6f3 100644 --- a/packages/frontend/src/pages/user/index.photos.vue +++ b/packages/frontend/src/pages/user/index.files.vue @@ -6,20 +6,24 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :max-height="300" :foldable="true"> <template #icon><i class="ti ti-photo"></i></template> - <template #header>{{ i18n.ts.images }}</template> + <template #header>{{ i18n.ts.files }}</template> <div :class="$style.root"> <MkLoading v-if="fetching"/> - <div v-if="!fetching && images.length > 0" :class="$style.stream"> - <MkA - v-for="image in images" - :key="image.note.id + image.file.id" - :class="$style.img" - :to="notePage(image.note)" - > - <ImgWithBlurhash :hash="image.file.blurhash" :src="thumbnail(image.file)" :title="image.file.name"/> - </MkA> + <div v-if="!fetching && files.length > 0" :class="$style.stream"> + <template v-for="file in files" :key="file.note.id + file.file.id"> + <div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.sensitive" @click="showingFiles.push(file.file.id)"> + <div> + <div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div> + <div>{{ i18n.ts.clickToShow }}</div> + </div> + </div> + <MkA v-else :class="$style.img" :to="notePage(file.note)"> + <!-- TODO: ç”»åƒä»¥å¤–ã®ãƒ•ã‚¡ã‚¤ãƒ«ã«å¯¾å¿œ --> + <ImgWithBlurhash :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name"/> + </MkA> + </template> </div> - <p v-if="!fetching && images.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p> + <p v-if="!fetching && files.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p> </div> </MkContainer> </template> @@ -40,10 +44,11 @@ const props = defineProps<{ }>(); let fetching = $ref(true); -let images = $ref<{ +let files = $ref<{ note: Misskey.entities.Note; file: Misskey.entities.DriveFile; }[]>([]); +let showingFiles = $ref<string[]>([]); function thumbnail(image: Misskey.entities.DriveFile): string { return defaultStore.state.disableShowingAnimatedImages @@ -52,24 +57,15 @@ function thumbnail(image: Misskey.entities.DriveFile): string { } onMounted(() => { - const image = [ - 'image/jpeg', - 'image/webp', - 'image/avif', - 'image/png', - 'image/gif', - 'image/apng', - 'image/vnd.mozilla.apng', - ]; os.api('users/notes', { userId: props.user.id, - fileType: image, + withFiles: true, excludeNsfw: defaultStore.state.nsfw !== 'ignore', - limit: 10, + limit: 15, }).then(notes => { for (const note of notes) { for (const file of note.files) { - images.push({ + files.push({ note, file, }); @@ -102,4 +98,9 @@ onMounted(() => { padding: 16px; text-align: center; } + +.sensitive { + display: grid; + place-items: center; +} </style> diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue index 42040f5304cc44b37bc4099f59ed236869db1f18..724fb4d11c7304d595c6d45d008095cc53e3a519 100644 --- a/packages/frontend/src/pages/user/index.timeline.vue +++ b/packages/frontend/src/pages/user/index.timeline.vue @@ -29,7 +29,7 @@ const props = defineProps<{ user: Misskey.entities.UserDetailed; }>(); -const include = ref<string | null>(null); +const include = ref<string | null>('all'); const pagination = { endpoint: 'users/notes' as const, @@ -38,6 +38,7 @@ const pagination = { userId: props.user.id, withRenotes: include.value === 'all', withReplies: include.value === 'all' || include.value === 'files', + withChannelNotes: include.value === 'all', withFiles: include.value === 'files', })), }; diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 415d2f19742202a085467f34e12d8a783b1895f7..6c33d0d8ee91acbe2a207dc291420e23aa76c2d6 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -126,18 +126,10 @@ export const routes = [{ path: '/import-export', name: 'import-export', component: page(() => import('./pages/settings/import-export.vue')), - }, { - path: '/instance-mute', - name: 'instance-mute', - component: page(() => import('./pages/settings/instance-mute.vue')), }, { path: '/mute-block', name: 'mute-block', component: page(() => import('./pages/settings/mute-block.vue')), - }, { - path: '/word-mute', - name: 'word-mute', - component: page(() => import('./pages/settings/word-mute.vue')), }, { path: '/api', name: 'api', @@ -435,6 +427,10 @@ export const routes = [{ path: '/proxy-account', name: 'proxy-account', component: page(() => import('./pages/admin/proxy-account.vue')), + }, { + path: '/external-services', + name: 'external-services', + component: page(() => import('./pages/admin/external-services.vue')), }, { path: '/other-settings', name: 'other-settings', @@ -471,6 +467,10 @@ export const routes = [{ path: '/my/drive', component: page(() => import('./pages/drive.vue')), loginRequired: true, +}, { + path: '/my/drive/file/:fileId', + component: page(() => import('./pages/drive.file.vue')), + loginRequired: true, }, { path: '/my/follow-requests', component: page(() => import('./pages/follow-requests.vue')), diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index 0964108249940f51d3da11d706e82badd25dd00c..8b2144a22f65e0a423543bcfd8abb15b14e31c2e 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -27,7 +27,7 @@ function rename(file: Misskey.entities.DriveFile) { function describe(file: Misskey.entities.DriveFile) { os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { - default: file.comment != null ? file.comment : '', + default: file.comment ?? '', file: file, }, { done: caption => { @@ -112,6 +112,11 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss text: i18n.ts.download, icon: 'ti ti-download', download: file.name, + }, null, { + type: 'link', + to: `/my/drive/file/${file.id}`, + text: i18n.ts._fileViewer.title, + icon: 'ti ti-file', }, null, { text: i18n.ts.delete, icon: 'ti ti-trash', diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index 62d9aab91bd61ef41c6cca3186b98b5114b0815a..e399145fc9552ca687ee47d4d5b73d4ea3d0451d 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -172,10 +172,6 @@ export function getNoteMenu(props: { }); } - function edit(): void { - os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel, updateMode: true }); - } - function toggleFavorite(favorite: boolean): void { claimAchievement('noteFavorited1'); os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { @@ -356,11 +352,6 @@ export function getNoteMenu(props: { ), ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ null, - appearNote.userId === $i.id && $i.policies.canEditNote ? { - icon: 'ti ti-edit', - text: i18n.ts.edit, - action: edit, - } : undefined, appearNote.userId === $i.id ? { icon: 'ti ti-edit', text: i18n.ts.deleteAndEdit, diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 128cbafb1568d42cf4c2928edffa1ece41247cbf..be514be5b1f6999f32e33abcb3d974099891e9d3 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -80,6 +80,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router }); } + async function toggleWithReplies() { + os.apiWithDialog('following/update', { + userId: user.id, + withReplies: !user.withReplies, + }).then(() => { + user.withReplies = !user.withReplies; + }); + } + async function toggleNotify() { os.apiWithDialog('following/update', { userId: user.id, @@ -282,6 +291,10 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router // フォãƒãƒ¼ã—ãŸã¨ã—ã¦ã‚‚ user.isFollowing ã¯ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ æ›´æ–°ã•ã‚Œãªã„ã®ã§ä¸ä¾¿ãªãŸã‚ //if (user.isFollowing) { menu = menu.concat([{ + icon: user.withReplies ? 'ti ti-messages-off' : 'ti ti-messages', + text: user.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline, + action: toggleWithReplies, + }, { icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off', text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes, action: toggleNotify, diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 1ef41b47d35493fddc5cb17378ac8048e1665760..1ef075818ffb58eefe0f497c6c50187688617323 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -7,10 +7,6 @@ import { markRaw } from 'vue'; import { Storage } from '@/pizzax.js'; export const soundConfigStore = markRaw(new Storage('sound', { - mediaVolume: { - where: 'device', - default: 0.5, - }, sound_masterVolume: { where: 'device', default: 0.3, @@ -27,14 +23,6 @@ export const soundConfigStore = markRaw(new Storage('sound', { 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 }, diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index 1c924e774f710a80beda07a1da443a57c6d6a82f..b6383487c92d15047981bfcd37cfedde6b050be2 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -5,7 +5,11 @@ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; -import { globalEvents } from '@/events'; +import { deepClone } from './clone.js'; +import { globalEvents } from '@/events.js'; +import lightTheme from '@/themes/_light.json5'; +import darkTheme from '@/themes/_dark.json5'; +import { miLocalStorage } from '@/local-storage.js'; export type Theme = { id: string; @@ -16,11 +20,6 @@ export type Theme = { props: Record<string, string>; }; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; -import { deepClone } from './clone'; -import { miLocalStorage } from '@/local-storage.js'; - export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); export const getBuiltinThemes = () => Promise.all( @@ -101,18 +100,11 @@ export function applyTheme(theme: Theme, persist = true) { function compile(theme: Theme): Record<string, string> { function getColor(val: string): tinycolor.Instance { - // ref (prop) - if (val[0] === '@') { + if (val[0] === '@') { // ref (prop) return getColor(theme.props[val.substring(1)]); - } - - // ref (const) - else if (val[0] === '$') { + } else if (val[0] === '$') { // ref (const) return getColor(theme.props[val]); - } - - // func - else if (val[0] === ':') { + } else if (val[0] === ':') { // func const parts = val.split('<'); const func = parts.shift().substring(1); const arg = parseFloat(parts.shift()); diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts index a4c913749ee5fa52bf456c46935c202e0664a082..c6185325706539a2a126de00c9ac90ffef935a3c 100644 --- a/packages/frontend/src/scripts/use-note-capture.ts +++ b/packages/frontend/src/scripts/use-note-capture.ts @@ -71,13 +71,6 @@ export function useNoteCapture(props: { break; } - case 'updated': { - note.value.updatedAt = new Date().toISOString(); - note.value.cw = body.cw; - note.value.text = body.text; - break; - } - case 'deleted': { props.isDeletedRef.value = true; break; diff --git a/packages/frontend/src/scripts/use-tooltip.ts b/packages/frontend/src/scripts/use-tooltip.ts index 0a82997728b17e851988797c7283813b218e4793..17ea380db06cbe36f5447279ef32c3d1432f17f7 100644 --- a/packages/frontend/src/scripts/use-tooltip.ts +++ b/packages/frontend/src/scripts/use-tooltip.ts @@ -21,6 +21,8 @@ export function useTooltip( let changeShowingState: (() => void) | null; + let autoHidingTimer; + const open = () => { close(); if (!isHovering) return; @@ -33,6 +35,16 @@ export function useTooltip( changeShowingState = () => { showing.value = false; }; + + autoHidingTimer = window.setInterval(() => { + if (!document.body.contains(elRef.value)) { + if (!isHovering) return; + isHovering = false; + window.clearTimeout(timeoutId); + close(); + window.clearInterval(autoHidingTimer); + } + }, 1000); }; const close = () => { @@ -53,6 +65,7 @@ export function useTooltip( if (!isHovering) return; isHovering = false; window.clearTimeout(timeoutId); + window.clearInterval(autoHidingTimer); close(); }; @@ -67,6 +80,7 @@ export function useTooltip( if (!isHovering) return; isHovering = false; window.clearTimeout(timeoutId); + window.clearInterval(autoHidingTimer); close(); }; diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index e715088d03c96ba3a769d7c2fb9f2c74ae70ec25..58730c7cef044260092420c39045d8c0908567de 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -5,7 +5,7 @@ import { markRaw, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { miLocalStorage } from './local-storage'; +import { miLocalStorage } from './local-storage.js'; import { Storage } from '@/pizzax.js'; interface PostFormAction { @@ -101,10 +101,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null, }, - mutedWords: { - where: 'account', - default: [], - }, mutedAds: { where: 'account', default: [] as string[], diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend/src/themes/_dark.json5 index 5ef6adb085e53dbac4178fcab63898c2ecfd9868..3f5822977abcb6d978b37de025cdab8c00449c68 100644 --- a/packages/frontend/src/themes/_dark.json5 +++ b/packages/frontend/src/themes/_dark.json5 @@ -54,9 +54,6 @@ infoWarnBg: '#42321c', infoWarnFg: '#ffbd3e', switchBg: 'rgba(255, 255, 255, 0.15)', - cwBg: '#687390', - cwFg: '#393f4f', - cwHoverBg: '#707b97', buttonBg: 'rgba(255, 255, 255, 0.05)', buttonHoverBg: 'rgba(255, 255, 255, 0.1)', buttonGradateA: '@accent', diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend/src/themes/_light.json5 index 32f3c7490917f59df4783c8c6397e5d438fb365f..6ebfcaafeb5319b3b557ff02e83af2c3ebaad587 100644 --- a/packages/frontend/src/themes/_light.json5 +++ b/packages/frontend/src/themes/_light.json5 @@ -54,9 +54,6 @@ infoWarnBg: '#fff0db', infoWarnFg: '#8f6e31', switchBg: 'rgba(0, 0, 0, 0.15)', - cwBg: '#b1b9c1', - cwFg: '#fff', - cwHoverBg: '#bbc4ce', buttonBg: 'rgba(0, 0, 0, 0.05)', buttonHoverBg: 'rgba(0, 0, 0, 0.1)', buttonGradateA: '@accent', diff --git a/packages/frontend/src/themes/d-astro.json5 b/packages/frontend/src/themes/d-astro.json5 index 09a9ead1a2e3398fc367e2dc615ece71c80cdf80..fee25cc4a4205d5b2c40b827650b8c1cadbaa720 100644 --- a/packages/frontend/src/themes/d-astro.json5 +++ b/packages/frontend/src/themes/d-astro.json5 @@ -6,8 +6,6 @@ props: { bg: '#232125', fg: '#efdab9', - cwBg: '#687390', - cwFg: '#393f4f', link: '#78b0a0', warn: '#ecb637', badge: '#31b1ce', @@ -29,7 +27,6 @@ success: '#86b300', buttonBg: 'rgba(255, 255, 255, 0.05)', acrylicBg: ':alpha<0.5<@bg', - cwHoverBg: '#707b97', indicator: '@accent', mentionMe: '#fb5d38', messageBg: '@bg', diff --git a/packages/frontend/src/themes/d-u0.json5 b/packages/frontend/src/themes/d-u0.json5 index ed776746a88a7238259386d38d3cfd6f9d644ed8..3bd0b9483c3df1aee7ed7f1004539acde9062e65 100644 --- a/packages/frontend/src/themes/d-u0.json5 +++ b/packages/frontend/src/themes/d-u0.json5 @@ -21,8 +21,6 @@ X15: ':alpha<0<@panel', X16: ':alpha<0.7<@panel', X17: ':alpha<0.8<@bg', - cwBg: '#687390', - cwFg: '#393f4f', link: '@accent', warn: '#ecb637', badge: '#31b1ce', @@ -46,7 +44,6 @@ buttonBg: 'rgba(255, 255, 255, 0.05)', switchBg: 'rgba(255, 255, 255, 0.15)', acrylicBg: ':alpha<0.5<@bg', - cwHoverBg: '#707b97', indicator: '@accent', mentionMe: '@mention', messageBg: '@bg', diff --git a/packages/frontend/src/themes/l-u0.json5 b/packages/frontend/src/themes/l-u0.json5 index b77b15e3f0ad1c9d3fbb64d6976fd9d7a35acf51..dbc777d49368107c638f60e7368e1c3b00563c8c 100644 --- a/packages/frontend/src/themes/l-u0.json5 +++ b/packages/frontend/src/themes/l-u0.json5 @@ -21,8 +21,6 @@ X15: ':alpha<0<@panel', X16: ':alpha<0.7<@panel', X17: ':alpha<0.8<@bg', - cwBg: '#687390', - cwFg: '#393f4f', link: '@accent', warn: '#ecb637', badge: '#31b1ce', @@ -46,7 +44,6 @@ buttonBg: '#0000000d', switchBg: 'rgba(255, 255, 255, 0.15)', acrylicBg: ':alpha<0.5<@bg', - cwHoverBg: '#707b97', indicator: '@accent', mentionMe: '@mention', messageBg: '@bg', diff --git a/packages/frontend/src/themes/l-vivid.json5 b/packages/frontend/src/themes/l-vivid.json5 index 822ef948dd8c912b1119f178d693d6f16c4929e9..3368855b5e96896449a95c6227cc3e87862551d4 100644 --- a/packages/frontend/src/themes/l-vivid.json5 +++ b/packages/frontend/src/themes/l-vivid.json5 @@ -9,8 +9,6 @@ props: { bg: '#fafafa', fg: '#444', - cwBg: '#b1b9c1', - cwFg: '#fff', link: '#ff9400', warn: '#ecb637', badge: '#31b1ce', @@ -32,7 +30,6 @@ success: '#86b300', buttonBg: 'rgba(0, 0, 0, 0.05)', acrylicBg: ':alpha<0.5<@bg', - cwHoverBg: '#bbc4ce', indicator: '@accent', mentionMe: '@mention', messageBg: '@bg', diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index ca4a71a67f0fd4078856d4ea45a3bdc207a792a6..e075e05db367d93d30e07dea62b85f13c4b3ebaf 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -68,7 +68,25 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.manageCustomEmojis, icon: 'ti ti-icons', } : undefined], - }, null, { + }, null, (instance.impressumUrl) ? { + text: i18n.ts.impressum, + icon: 'ti ti-file-invoice', + action: () => { + window.open(instance.impressumUrl, '_blank'); + }, + } : undefined, (instance.tosUrl) ? { + text: i18n.ts.termsOfService, + icon: 'ti ti-notebook', + action: () => { + window.open(instance.tosUrl, '_blank'); + }, + } : undefined, (instance.privacyPolicyUrl) ? { + text: i18n.ts.privacyPolicy, + icon: 'ti ti-shield-lock', + action: () => { + window.open(instance.privacyPolicyUrl, '_blank'); + }, + } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : null, { text: i18n.ts.help, icon: 'ti ti-help-circle', action: () => { diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index 49fdf4d314659bd1a367a36479b9bd89da90aa97..b2a44ac96b12aae611b5e196adeae93613ffe651 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -31,7 +31,6 @@ export type Column = { excludeTypes?: typeof notificationTypes[number][]; tl?: 'home' | 'local' | 'social' | 'global'; withRenotes?: boolean; - withReplies?: boolean; onlyFiles?: boolean; }; diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index c43ccdfabb4ec087cfa4eba1a1be93d958fc00b9..14bc6917a35d36eb09c3dd9c1b4d041ab9d63702 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -9,12 +9,12 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> - <MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId"/> + <MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" :withRenotes="withRenotes"/> </XColumn> </template> <script lang="ts" setup> -import { } from 'vue'; +import { watch } from 'vue'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store'; import MkTimeline from '@/components/MkTimeline.vue'; @@ -27,11 +27,18 @@ const props = defineProps<{ }>(); let timeline = $shallowRef<InstanceType<typeof MkTimeline>>(); +const withRenotes = $ref(props.column.withRenotes ?? true); if (props.column.listId == null) { setList(); } +watch($$(withRenotes), v => { + updateColumn(props.column.id, { + withRenotes: v, + }); +}); + async function setList() { const lists = await os.api('users/lists/list'); const { canceled, result: list } = await os.select({ @@ -62,5 +69,10 @@ const menu = [ text: i18n.ts.editList, action: editList, }, + { + type: 'switch', + text: i18n.ts.showRenotes, + ref: $$(withRenotes), + }, ]; </script> diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index aad73d73a1e3929abaf216ba42b5c12336383a0a..847752247e982ee029c8cc142a36f9a9d0e6b106 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -23,10 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkTimeline v-else-if="column.tl" ref="timeline" - :key="column.tl + withRenotes + withReplies + onlyFiles" + :key="column.tl + withRenotes + onlyFiles" :src="column.tl" :withRenotes="withRenotes" - :withReplies="withReplies" :onlyFiles="onlyFiles" /> </XColumn> @@ -52,7 +51,6 @@ let disabled = $ref(false); const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable)); const withRenotes = $ref(props.column.withRenotes ?? true); -const withReplies = $ref(props.column.withReplies ?? false); const onlyFiles = $ref(props.column.onlyFiles ?? false); watch($$(withRenotes), v => { @@ -61,12 +59,6 @@ watch($$(withRenotes), v => { }); }); -watch($$(withReplies), v => { - updateColumn(props.column.id, { - withReplies: v, - }); -}); - watch($$(onlyFiles), v => { updateColumn(props.column.id, { onlyFiles: v, @@ -115,10 +107,6 @@ const menu = [{ type: 'switch', text: i18n.ts.showRenotes, ref: $$(withRenotes), -}, { - type: 'switch', - text: i18n.ts.withReplies, - ref: $$(withReplies), }, { type: 'switch', text: i18n.ts.fileAttachedOnly, diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index f0fc47c2073e6f910999cd915bf4d95111d0d8ce..1a0bbeac78ea3b2c08f7a420a6b8ca1efabe619c 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1381,10 +1381,6 @@ export type Endpoints = { req: TODO; res: TODO; }; - 'i/get-word-muted-notes-count': { - req: TODO; - res: TODO; - }; 'i/import-following': { req: TODO; res: TODO; @@ -2412,6 +2408,8 @@ type LiteInstanceMetadata = { tosUrl: string | null; repositoryUrl: string; feedbackUrl: string; + impressumUrl: string | null; + privacyPolicyUrl: string | null; disableRegistration: boolean; disableLocalTimeline: boolean; disableGlobalTimeline: boolean; @@ -2450,6 +2448,7 @@ type LiteInstanceMetadata = { url: string; imageUrl: string; }[]; + notesPerOneAd: number; translatorAvailable: boolean; serverRules: string[]; }; @@ -2643,7 +2642,6 @@ export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"]; type Note = { id: ID; createdAt: DateString; - updatedAt?: DateString | null; text: string | null; cw: string | null; user: User; @@ -2757,6 +2755,9 @@ type Notification_2 = { invitation: UserGroup; user: User; userId: User['id']; +} | { + type: 'achievementEarned'; + achievement: string; } | { type: 'app'; header?: string | null; @@ -2767,7 +2768,7 @@ type Notification_2 = { }); // @public (undocumented) -export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app"]; +export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "achievementEarned"]; // @public (undocumented) type OriginType = 'combined' | 'local' | 'remote'; @@ -2981,9 +2982,9 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u // // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts -// src/api.types.ts:631:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts +// src/api.types.ts:630:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/entities.ts:107:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts -// src/entities.ts:595:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts +// src/entities.ts:600:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index cdbb5f38ce41a0349589a3dc3bc5d3ffcb42b1e6..1b27380019f5eac46aece380bba8116950d92efb 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -20,13 +20,13 @@ "url": "git+https://github.com/misskey-dev/misskey.js.git" }, "devDependencies": { - "@microsoft/api-extractor": "7.37.2", + "@microsoft/api-extractor": "7.38.0", "@swc/jest": "0.2.29", "@types/jest": "29.5.5", - "@types/node": "20.7.1", - "@typescript-eslint/eslint-plugin": "6.7.3", - "@typescript-eslint/parser": "6.7.3", - "eslint": "8.50.0", + "@types/node": "20.8.4", + "@typescript-eslint/eslint-plugin": "6.7.5", + "@typescript-eslint/parser": "6.7.5", + "eslint": "8.51.0", "jest": "29.7.0", "jest-fetch-mock": "3.0.3", "jest-websocket-mock": "2.5.0", @@ -39,7 +39,7 @@ ], "dependencies": { "@swc/cli": "0.1.62", - "@swc/core": "1.3.90", + "@swc/core": "1.3.92", "eventemitter3": "5.0.1", "reconnecting-websocket": "4.4.0" } diff --git a/packages/misskey-js/src/api.ts b/packages/misskey-js/src/api.ts index 974cb35ace60738fa8675859093797d4cb21d36e..9415e692e37bffe0ba839c163df5729c7d515ff5 100644 --- a/packages/misskey-js/src/api.ts +++ b/packages/misskey-js/src/api.ts @@ -67,8 +67,7 @@ export class APIClient { IsCaseMatched<E, P, 8> extends true ? GetCaseResult<E, P, 8> : IsCaseMatched<E, P, 9> extends true ? GetCaseResult<E, P, 9> : Endpoints[E]['res']['$switch']['$default'] - : Endpoints[E]['res']> - { + : Endpoints[E]['res']> { const promise = new Promise((resolve, reject) => { this.fetch(`${this.origin}/api/${endpoint}`, { method: 'POST', diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index e69d8324a1cbbf92c5566a52544c35234c5f7487..a7a2ea1b36ee75cd75a0caef39dde87bd63a7e1c 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -371,7 +371,6 @@ export type Endpoints = { 'i/favorites': { req: { limit?: number; sinceId?: NoteFavorite['id']; untilId?: NoteFavorite['id']; }; res: NoteFavorite[]; }; 'i/gallery/likes': { req: TODO; res: TODO; }; 'i/gallery/posts': { req: TODO; res: TODO; }; - 'i/get-word-muted-notes-count': { req: TODO; res: TODO; }; 'i/import-following': { req: TODO; res: TODO; }; 'i/import-user-lists': { req: TODO; res: TODO; }; 'i/move': { req: TODO; res: TODO; }; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 271a64274ff9c85f8c342aefa54472c939ae1bb0..c4ddead823aed2570f097de7ff959877a8952d46 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -1,4 +1,4 @@ -export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const; +export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'achievementEarned'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; @@ -189,6 +189,9 @@ export type ModerationLogPayloads = { deleteUserAnnouncement: { announcementId: string; announcement: any; + userId: string; + userUsername: string; + userHost: string | null; }; resetPassword: { userId: string; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index e6bac2a5f46f972a6619c393293e2568f83de9fa..aed242d8aa8b78c07dec9fa2340f1b52ab0fdca1 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -177,7 +177,6 @@ export type GalleryPost = { export type Note = { id: ID; createdAt: DateString; - updatedAt?: DateString | null; text: string | null; cw: string | null; user: User; @@ -278,6 +277,9 @@ export type Notification = { invitation: UserGroup; user: User; userId: User['id']; +} | { + type: 'achievementEarned'; + achievement: string; } | { type: 'app'; header?: string | null; @@ -323,6 +325,8 @@ export type LiteInstanceMetadata = { tosUrl: string | null; repositoryUrl: string; feedbackUrl: string; + impressumUrl: string | null; + privacyPolicyUrl: string | null; disableRegistration: boolean; disableLocalTimeline: boolean; disableGlobalTimeline: boolean; @@ -361,6 +365,7 @@ export type LiteInstanceMetadata = { url: string; imageUrl: string; }[]; + notesPerOneAd: number; translatorAvailable: boolean; serverRules: string[]; }; diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts index ce29a000329aa2cb273065f9530730d9c852cae8..96ac7787e1e18415850400a8fde41c0fafee833f 100644 --- a/packages/misskey-js/src/streaming.types.ts +++ b/packages/misskey-js/src/streaming.types.ts @@ -133,13 +133,6 @@ export type NoteUpdatedEvent = { body: { deletedAt: string; }; -} | { - id: Note['id']; - type: 'updated'; - body: { - cw: string | null; - text: string; - }; } | { id: Note['id']; type: 'pollVoted'; diff --git a/packages/shared/.eslintrc.js b/packages/shared/.eslintrc.js index 1ecad7ab759ddf586528ee1eecbda4c50fa03256..c578894f60f8ef008c30cdee3ef43af45c21972d 100644 --- a/packages/shared/.eslintrc.js +++ b/packages/shared/.eslintrc.js @@ -38,6 +38,9 @@ module.exports = { 'before': true, 'after': true, }], + 'brace-style': ['error', '1tbs', { + 'allowSingleLine': true, + }], 'padded-blocks': ['error', 'never'], /* TODO: path aliasを使ã‚ãªã„ã¨warnã™ã‚‹ 'no-restricted-imports': ['warn', { diff --git a/packages/sw/package.json b/packages/sw/package.json index 4499e9f38e2c27a84de52db94cb4430739c9d5e8..24878a6e2f927108f2e822a97bd23440afba025c 100644 --- a/packages/sw/package.json +++ b/packages/sw/package.json @@ -14,9 +14,9 @@ "misskey-js": "workspace:*" }, "devDependencies": { - "@typescript-eslint/parser": "6.7.3", + "@typescript-eslint/parser": "6.7.5", "@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67", - "eslint": "8.50.0", + "eslint": "8.51.0", "eslint-plugin-import": "2.28.1", "typescript": "5.2.2" }, diff --git a/packages/sw/src/scripts/operations.ts b/packages/sw/src/scripts/operations.ts index be4f066b5fe8902753a0b25c9d6ba842d2993362..0cbf4c7953dacb4d639bb79197bf1fb209223c85 100644 --- a/packages/sw/src/scripts/operations.ts +++ b/packages/sw/src/scripts/operations.ts @@ -15,7 +15,7 @@ import { getUrlWithLoginId } from '@/scripts/login-id.js'; export const cli = new Misskey.api.APIClient({ origin, fetch: (...args): Promise<Response> => fetch(...args) }); 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; + let account: { token: string; id: string } | void = undefined; if (userId) { account = await getAccountFromId(userId); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ce08699c0c00f06fae8131255a2c948a68507b7..5466d7b2088991c86e9460a1c66bd8b370c8ff9b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,8 +25,8 @@ importers: specifier: 8.4.31 version: 8.4.31 terser: - specifier: 5.20.0 - version: 5.20.0 + specifier: 5.21.0 + version: 5.21.0 typescript: specifier: 5.2.2 version: 5.2.2 @@ -36,11 +36,11 @@ importers: version: 4.4.0 devDependencies: '@typescript-eslint/eslint-plugin': - specifier: 6.7.3 - version: 6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.5 + version: 6.7.5(@typescript-eslint/parser@6.7.5)(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/parser': - specifier: 6.7.3 - version: 6.7.3(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.5 + version: 6.7.5(eslint@8.51.0)(typescript@5.2.2) cross-env: specifier: 7.0.3 version: 7.0.3 @@ -48,8 +48,8 @@ importers: specifier: 13.3.0 version: 13.3.0 eslint: - specifier: 8.50.0 - version: 8.50.0 + specifier: 8.51.0 + version: 8.51.0 start-server-and-test: specifier: 2.0.1 version: 2.0.1 @@ -99,14 +99,14 @@ importers: specifier: 8.2.0 version: 8.2.0 '@nestjs/common': - specifier: 10.2.6 - version: 10.2.6(reflect-metadata@0.1.13)(rxjs@7.8.1) + specifier: 10.2.7 + version: 10.2.7(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': - specifier: 10.2.6 - version: 10.2.6(@nestjs/common@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1) + specifier: 10.2.7 + version: 10.2.7(@nestjs/common@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/testing': - specifier: 10.2.6 - version: 10.2.6(@nestjs/common@10.2.6)(@nestjs/core@10.2.6) + specifier: 10.2.7 + version: 10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7) '@peertube/http-signature': specifier: 1.7.0 version: 1.7.0 @@ -121,10 +121,10 @@ importers: version: 2.1.5 '@swc/cli': specifier: 0.1.62 - version: 0.1.62(@swc/core@1.3.90)(chokidar@3.5.3) + version: 0.1.62(@swc/core@1.3.92)(chokidar@3.5.3) '@swc/core': - specifier: 1.3.90 - version: 1.3.90 + specifier: 1.3.92 + version: 1.3.92 accepts: specifier: 1.3.8 version: 1.3.8 @@ -147,8 +147,8 @@ importers: specifier: 1.20.2 version: 1.20.2 bullmq: - specifier: 4.11.4 - version: 4.11.4 + specifier: 4.12.3 + version: 4.12.3 cacheable-lookup: specifier: 7.0.0 version: 7.0.0 @@ -261,8 +261,8 @@ importers: specifier: 3.3.2 version: 3.3.2 nodemailer: - specifier: 6.9.5 - version: 6.9.5 + specifier: 6.9.6 + version: 6.9.6 nsfwjs: specifier: 2.4.2 version: 2.4.2(@tensorflow/tfjs@4.4.0) @@ -279,8 +279,8 @@ importers: specifier: 0.0.14 version: 0.0.14 otpauth: - specifier: 9.1.4 - version: 9.1.4 + specifier: 9.1.5 + version: 9.1.5 parse5: specifier: 7.1.2 version: 7.1.2 @@ -354,8 +354,8 @@ importers: specifier: github:misskey-dev/summaly version: github.com/misskey-dev/summaly/d2d8db49943ccb201c1b1b283e9d0a630519fac7 systeminformation: - specifier: 5.21.9 - version: 5.21.9 + specifier: 5.21.11 + version: 5.21.11 tinycolor2: specifier: 1.6.0 version: 1.6.0 @@ -489,7 +489,7 @@ importers: version: 8.0.0 '@swc/jest': specifier: 0.2.29 - version: 0.2.29(@swc/core@1.3.90) + version: 0.2.29(@swc/core@1.3.92) '@types/accepts': specifier: 1.3.5 version: 1.3.5 @@ -539,8 +539,8 @@ importers: specifier: 0.7.32 version: 0.7.32 '@types/node': - specifier: 20.7.1 - version: 20.7.1 + specifier: 20.8.4 + version: 20.8.4 '@types/node-fetch': specifier: 3.0.3 version: 3.0.3 @@ -557,8 +557,8 @@ importers: specifier: 0.1.0 version: 0.1.0 '@types/pg': - specifier: 8.10.3 - version: 8.10.3 + specifier: 8.10.4 + version: 8.10.4 '@types/pug': specifier: 2.0.7 version: 2.0.7 @@ -608,11 +608,11 @@ importers: specifier: 8.5.6 version: 8.5.6 '@typescript-eslint/eslint-plugin': - specifier: 6.7.3 - version: 6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.5 + version: 6.7.5(@typescript-eslint/parser@6.7.5)(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/parser': - specifier: 6.7.3 - version: 6.7.3(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.5 + version: 6.7.5(eslint@8.51.0)(typescript@5.2.2) aws-sdk-client-mock: specifier: 3.0.0 version: 3.0.0 @@ -620,17 +620,17 @@ importers: specifier: 7.0.3 version: 7.0.3 eslint: - specifier: 8.50.0 - version: 8.50.0 + specifier: 8.51.0 + version: 8.51.0 eslint-plugin-import: specifier: 2.28.1 - version: 2.28.1(@typescript-eslint/parser@6.7.3)(eslint@8.50.0) + version: 2.28.1(@typescript-eslint/parser@6.7.5)(eslint@8.51.0) execa: specifier: 8.0.1 version: 8.0.1 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.7.1) + version: 29.7.0(@types/node@20.8.4) jest-mock: specifier: 29.7.0 version: 29.7.0 @@ -647,17 +647,17 @@ importers: specifier: 2.1.1 version: 2.1.1 '@rollup/plugin-alias': - specifier: 5.0.0 - version: 5.0.0(rollup@3.29.4) + specifier: 5.0.1 + version: 5.0.1(rollup@4.0.2) '@rollup/plugin-json': - specifier: 6.0.0 - version: 6.0.0(rollup@3.29.4) + specifier: 6.0.1 + version: 6.0.1(rollup@4.0.2) '@rollup/plugin-replace': - specifier: 5.0.2 - version: 5.0.2(rollup@3.29.4) + specifier: 5.0.3 + version: 5.0.3(rollup@4.0.2) '@rollup/pluginutils': - specifier: 5.0.4 - version: 5.0.4(rollup@3.29.4) + specifier: 5.0.5 + version: 5.0.5(rollup@4.0.2) '@syuilo/aiscript': specifier: 0.16.0 version: 0.16.0 @@ -665,11 +665,11 @@ importers: specifier: 2.37.0 version: 2.37.0 '@vitejs/plugin-vue': - specifier: 4.3.4 - version: 4.3.4(vite@4.4.9)(vue@3.3.4) + specifier: 4.4.0 + version: 4.4.0(vite@4.4.11)(vue@3.3.4) '@vue-macros/reactivity-transform': specifier: 0.3.23 - version: 0.3.23(rollup@3.29.4)(vue@3.3.4) + version: 0.3.23(rollup@4.0.2)(vue@3.3.4) '@vue/compiler-sfc': specifier: 3.3.4 version: 3.3.4 @@ -707,8 +707,8 @@ importers: specifier: 2.0.1 version: 2.0.1(chart.js@4.4.0) chromatic: - specifier: 7.2.0 - version: 7.2.0 + specifier: 7.2.3 + version: 7.2.3 compare-versions: specifier: 6.1.0 version: 6.1.0 @@ -752,8 +752,8 @@ importers: specifier: workspace:* version: link:../misskey-js photoswipe: - specifier: 5.4.1 - version: 5.4.1 + specifier: 5.4.2 + version: 5.4.2 prismjs: specifier: 1.29.0 version: 1.29.0 @@ -764,14 +764,14 @@ importers: specifier: 0.2.1 version: 0.2.1 rollup: - specifier: 3.29.4 - version: 3.29.4 + specifier: 4.0.2 + version: 4.0.2 sanitize-html: specifier: 2.11.0 version: 2.11.0 sass: - specifier: 1.68.0 - version: 1.68.0 + specifier: 1.69.1 + version: 1.69.1 strict-event-emitter-types: specifier: 2.0.0 version: 2.0.0 @@ -809,8 +809,8 @@ importers: specifier: 1.8.1 version: 1.8.1 vite: - specifier: 4.4.9 - version: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + specifier: 4.4.11 + version: 4.4.11(@types/node@20.8.4)(sass@1.69.1)(terser@5.21.0) vue: specifier: 3.3.4 version: 3.3.4 @@ -822,59 +822,59 @@ importers: version: 4.1.0(vue@3.3.4) devDependencies: '@storybook/addon-actions': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-essentials': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-interactions': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-links': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-storysource': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/addons': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/blocks': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/core-events': - specifier: 7.4.5 - version: 7.4.5 + specifier: 7.4.6 + version: 7.4.6 '@storybook/jest': - specifier: 0.2.2 - version: 0.2.2(vitest@0.34.5) + specifier: 0.2.3 + version: 0.2.3(vitest@0.34.6) '@storybook/manager-api': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/preview-api': - specifier: 7.4.5 - version: 7.4.5 + specifier: 7.4.6 + version: 7.4.6 '@storybook/react': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) '@storybook/react-vite': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0)(rollup@3.29.4)(typescript@5.2.2)(vite@4.4.9) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0)(rollup@4.0.2)(typescript@5.2.2)(vite@4.4.11) '@storybook/testing-library': - specifier: 0.2.1 - version: 0.2.1 + specifier: 0.2.2 + version: 0.2.2 '@storybook/theming': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/types': - specifier: 7.4.5 - version: 7.4.5 + specifier: 7.4.6 + version: 7.4.6 '@storybook/vue3': - specifier: 7.4.5 - version: 7.4.5(@vue/compiler-core@3.3.4)(vue@3.3.4) + specifier: 7.4.6 + version: 7.4.6(@vue/compiler-core@3.3.4)(vue@3.3.4) '@storybook/vue3-vite': - specifier: 7.4.5 - version: 7.4.5(@vue/compiler-core@3.3.4)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(vite@4.4.9)(vue@3.3.4) + specifier: 7.4.6 + version: 7.4.6(@vue/compiler-core@3.3.4)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(vite@4.4.11)(vue@3.3.4) '@testing-library/vue': specifier: 7.0.0 version: 7.0.0(@vue/compiler-sfc@3.3.4)(vue@3.3.4) @@ -891,8 +891,8 @@ importers: specifier: 4.0.3 version: 4.0.3 '@types/node': - specifier: 20.7.1 - version: 20.7.1 + specifier: 20.8.4 + version: 20.8.4 '@types/punycode': specifier: 2.1.0 version: 2.1.0 @@ -906,8 +906,8 @@ importers: specifier: 1.4.4 version: 1.4.4 '@types/uuid': - specifier: 9.0.4 - version: 9.0.4 + specifier: 9.0.5 + version: 9.0.5 '@types/websocket': specifier: 1.0.7 version: 1.0.7 @@ -915,14 +915,14 @@ importers: specifier: 8.5.6 version: 8.5.6 '@typescript-eslint/eslint-plugin': - specifier: 6.7.3 - version: 6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.5 + version: 6.7.5(@typescript-eslint/parser@6.7.5)(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/parser': - specifier: 6.7.3 - version: 6.7.3(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.5 + version: 6.7.5(eslint@8.51.0)(typescript@5.2.2) '@vitest/coverage-v8': - specifier: 0.34.5 - version: 0.34.5(vitest@0.34.5) + specifier: 0.34.6 + version: 0.34.6(vitest@0.34.6) '@vue/runtime-core': specifier: 3.3.4 version: 3.3.4 @@ -936,14 +936,14 @@ importers: specifier: 13.3.0 version: 13.3.0 eslint: - specifier: 8.50.0 - version: 8.50.0 + specifier: 8.51.0 + version: 8.51.0 eslint-plugin-import: specifier: 2.28.1 - version: 2.28.1(@typescript-eslint/parser@6.7.3)(eslint@8.50.0) + version: 2.28.1(@typescript-eslint/parser@6.7.5)(eslint@8.51.0) eslint-plugin-vue: specifier: 9.17.0 - version: 9.17.0(eslint@8.50.0) + version: 9.17.0(eslint@8.51.0) fast-glob: specifier: 3.3.1 version: 3.3.1 @@ -954,11 +954,11 @@ importers: specifier: 4.0.5 version: 4.0.5 msw: - specifier: 1.3.1 - version: 1.3.1(typescript@5.2.2) + specifier: 1.3.2 + version: 1.3.2(typescript@5.2.2) msw-storybook-addon: specifier: 1.8.0 - version: 1.8.0(msw@1.3.1) + version: 1.8.0(msw@1.3.2) nodemon: specifier: 3.0.1 version: 3.0.1 @@ -975,11 +975,11 @@ importers: specifier: 2.0.1 version: 2.0.1 storybook: - specifier: 7.4.5 - version: 7.4.5 + specifier: 7.4.6 + version: 7.4.6 storybook-addon-misskey-theme: specifier: github:misskey-dev/storybook-addon-misskey-theme - version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.4.5)(@storybook/components@7.4.4)(@storybook/core-events@7.4.5)(@storybook/manager-api@7.4.5)(@storybook/preview-api@7.4.5)(@storybook/theming@7.4.5)(@storybook/types@7.4.5)(react-dom@18.2.0)(react@18.2.0) + version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.4.6)(@storybook/components@7.4.6)(@storybook/core-events@7.4.6)(@storybook/manager-api@7.4.6)(@storybook/preview-api@7.4.6)(@storybook/theming@7.4.6)(@storybook/types@7.4.6)(react-dom@18.2.0)(react@18.2.0) summaly: specifier: github:misskey-dev/summaly version: github.com/misskey-dev/summaly/d2d8db49943ccb201c1b1b283e9d0a630519fac7 @@ -987,26 +987,26 @@ importers: specifier: 1.0.3 version: 1.0.3 vitest: - specifier: 0.34.5 - version: 0.34.5(happy-dom@10.0.3)(sass@1.68.0)(terser@5.20.0) + specifier: 0.34.6 + version: 0.34.6(happy-dom@10.0.3)(sass@1.69.1)(terser@5.21.0) vitest-fetch-mock: specifier: 0.2.2 - version: 0.2.2(vitest@0.34.5) + version: 0.2.2(vitest@0.34.6) vue-eslint-parser: - specifier: 9.3.1 - version: 9.3.1(eslint@8.50.0) + specifier: 9.3.2 + version: 9.3.2(eslint@8.51.0) vue-tsc: - specifier: 1.8.15 - version: 1.8.15(typescript@5.2.2) + specifier: 1.8.18 + version: 1.8.18(typescript@5.2.2) packages/misskey-js: dependencies: '@swc/cli': specifier: 0.1.62 - version: 0.1.62(@swc/core@1.3.90)(chokidar@3.5.3) + version: 0.1.62(@swc/core@1.3.92)(chokidar@3.5.3) '@swc/core': - specifier: 1.3.90 - version: 1.3.90 + specifier: 1.3.92 + version: 1.3.92 eventemitter3: specifier: 5.0.1 version: 5.0.1 @@ -1015,29 +1015,29 @@ importers: version: 4.4.0 devDependencies: '@microsoft/api-extractor': - specifier: 7.37.2 - version: 7.37.2(@types/node@20.7.1) + specifier: 7.38.0 + version: 7.38.0(@types/node@20.8.4) '@swc/jest': specifier: 0.2.29 - version: 0.2.29(@swc/core@1.3.90) + version: 0.2.29(@swc/core@1.3.92) '@types/jest': specifier: 29.5.5 version: 29.5.5 '@types/node': - specifier: 20.7.1 - version: 20.7.1 + specifier: 20.8.4 + version: 20.8.4 '@typescript-eslint/eslint-plugin': - specifier: 6.7.3 - version: 6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.5 + version: 6.7.5(@typescript-eslint/parser@6.7.5)(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/parser': - specifier: 6.7.3 - version: 6.7.3(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.5 + version: 6.7.5(eslint@8.51.0)(typescript@5.2.2) eslint: - specifier: 8.50.0 - version: 8.50.0 + specifier: 8.51.0 + version: 8.51.0 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.7.1) + version: 29.7.0(@types/node@20.8.4) jest-fetch-mock: specifier: 3.0.3 version: 3.0.3 @@ -1067,17 +1067,17 @@ importers: version: link:../misskey-js devDependencies: '@typescript-eslint/parser': - specifier: 6.7.3 - version: 6.7.3(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.5 + version: 6.7.5(eslint@8.51.0)(typescript@5.2.2) '@typescript/lib-webworker': specifier: npm:@types/serviceworker@0.0.67 version: /@types/serviceworker@0.0.67 eslint: - specifier: 8.50.0 - version: 8.50.0 + specifier: 8.51.0 + version: 8.51.0 eslint-plugin-import: specifier: 2.28.1 - version: 2.28.1(@typescript-eslint/parser@6.7.3)(eslint@8.50.0) + version: 2.28.1(@typescript-eslint/parser@6.7.5)(eslint@8.51.0) typescript: specifier: 5.2.2 version: 5.2.2 @@ -3686,13 +3686,13 @@ packages: dev: false optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.50.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.51.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.50.0 + eslint: 8.51.0 eslint-visitor-keys: 3.4.3 dev: true @@ -3718,8 +3718,8 @@ packages: - supports-color dev: true - /@eslint/js@8.50.0: - resolution: {integrity: sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==} + /@eslint/js@8.51.0: + resolution: {integrity: sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -4008,7 +4008,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.4 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -4029,14 +4029,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.4 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.7.1 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.7.1) + jest-config: 29.7.0(@types/node@20.8.4) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -4071,7 +4071,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.4 jest-mock: 29.7.0 dev: true @@ -4098,7 +4098,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.7.1 + '@types/node': 20.8.4 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -4131,7 +4131,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.18 - '@types/node': 20.7.1 + '@types/node': 20.8.4 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -4225,7 +4225,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.7.1 + '@types/node': 20.8.4 '@types/yargs': 16.0.5 chalk: 4.1.2 dev: true @@ -4237,12 +4237,12 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.7.1 + '@types/node': 20.8.4 '@types/yargs': 17.0.19 chalk: 4.1.2 dev: true - /@joshwooding/vite-plugin-react-docgen-typescript@0.2.1(typescript@5.2.2)(vite@4.4.9): + /@joshwooding/vite-plugin-react-docgen-typescript@0.2.1(typescript@5.2.2)(vite@4.4.11): resolution: {integrity: sha512-ou4ZJSXMMWHqGS4g8uNRbC5TiTWxAgQZiVucoUrOCWuPrTbkpJbmVyIi9jU72SBry7gQtuMEDp4YR8EEXAg7VQ==} peerDependencies: typescript: '>= 4.3.x' @@ -4256,7 +4256,7 @@ packages: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.2.2) typescript: 5.2.2 - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.4)(sass@1.69.1)(terser@5.21.0) dev: true /@jridgewell/gen-mapping@0.3.2: @@ -4341,24 +4341,24 @@ packages: react: 18.2.0 dev: true - /@microsoft/api-extractor-model@7.28.2(@types/node@20.7.1): + /@microsoft/api-extractor-model@7.28.2(@types/node@20.8.4): resolution: {integrity: sha512-vkojrM2fo3q4n4oPh4uUZdjJ2DxQ2+RnDQL/xhTWSRUNPF6P4QyrvY357HBxbnltKcYu+nNNolVqc6TIGQ73Ig==} dependencies: '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 3.61.0(@types/node@20.7.1) + '@rushstack/node-core-library': 3.61.0(@types/node@20.8.4) transitivePeerDependencies: - '@types/node' dev: true - /@microsoft/api-extractor@7.37.2(@types/node@20.7.1): - resolution: {integrity: sha512-b4tr1rTto9/utTjbuqRwfQP2mzP0ACCmJMUY0JIOfOQ3tewGOkMCIRpIS5kcv5/nURekoAY06hNwHmkVsv/s1g==} + /@microsoft/api-extractor@7.38.0(@types/node@20.8.4): + resolution: {integrity: sha512-e1LhZYnfw+JEebuY2bzhw0imDCl1nwjSThTrQqBXl40hrVo6xm3j/1EpUr89QyzgjqmAwek2ZkIVZbrhaR+cqg==} hasBin: true dependencies: - '@microsoft/api-extractor-model': 7.28.2(@types/node@20.7.1) + '@microsoft/api-extractor-model': 7.28.2(@types/node@20.8.4) '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 3.61.0(@types/node@20.7.1) + '@rushstack/node-core-library': 3.61.0(@types/node@20.8.4) '@rushstack/rig-package': 0.5.1 '@rushstack/ts-command-line': 4.16.1 colors: 1.2.5 @@ -4478,8 +4478,8 @@ packages: tar-fs: 2.1.1 dev: true - /@nestjs/common@10.2.6(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-ma8R7n+FXsWM4XF9QXjjrsRceyRzid/xKmNKVOa/sTJntkVG8lL71BHBEfjtFvO6EJUqjs/15LbDc0iaN5nCwA==} + /@nestjs/common@10.2.7(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-cUtCRXiUstDmh4bSBhVbq4cI439Gngp4LgLGLBmd5dqFQodfXKnSD441ldYfFiLz4rbUsnoMJz/8ZjuIEI+B7A==} peerDependencies: class-transformer: '*' class-validator: '*' @@ -4498,8 +4498,8 @@ packages: uid: 2.0.2 dev: false - /@nestjs/core@10.2.6(@nestjs/common@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-oGQ2CoBeFRT7egG47MFqS89xlXBTIRZBkRpKRTPMftEfL1RMXhXIcIIaGfzp11wx6qxrBVxBXpVLM09oaqHpaQ==} + /@nestjs/core@10.2.7(@nestjs/common@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-5GSu53QUUcwX17sNmlJPa1I0wIeAZOKbedyVuQx0ZAwWVa9g0wJBbsNP+R4EJ+j5Dkdzt/8xkiZvnKt8RFRR8g==} requiresBuild: true peerDependencies: '@nestjs/common': ^10.0.0 @@ -4516,7 +4516,7 @@ packages: '@nestjs/websockets': optional: true dependencies: - '@nestjs/common': 10.2.6(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.7(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -4529,8 +4529,8 @@ packages: - encoding dev: false - /@nestjs/testing@10.2.6(@nestjs/common@10.2.6)(@nestjs/core@10.2.6): - resolution: {integrity: sha512-uxlxHhpSvG4yDTPmuPneoQL1/UnBkOkzE+Zaz6bwURg7lc3uS4ZsXl75OL3pYaJH37rHYXYT9bGcYSpxVbwIrg==} + /@nestjs/testing@10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7): + resolution: {integrity: sha512-d2SIqiJIf/7NSILeNNWSdRvTTpHSouGgisGHwf5PVDC7z4/yXZw/wPO9eJhegnxFlqk6n2LW4QBTmMzbqjAfHA==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 @@ -4542,8 +4542,8 @@ packages: '@nestjs/platform-express': optional: true dependencies: - '@nestjs/common': 10.2.6(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.6(@nestjs/common@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.7(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) tslib: 2.6.2 dev: false @@ -5189,51 +5189,51 @@ packages: '@babel/runtime': 7.22.10 dev: true - /@rollup/plugin-alias@5.0.0(rollup@3.29.4): - resolution: {integrity: sha512-l9hY5chSCjuFRPsnRm16twWBiSApl2uYFLsepQYwtBuAxNMQ/1dJqADld40P0Jkqm65GRTLy/AC6hnpVebtLsA==} + /@rollup/plugin-alias@5.0.1(rollup@4.0.2): + resolution: {integrity: sha512-JObvbWdOHoMy9W7SU0lvGhDtWq9PllP5mjpAy+TUslZG/WzOId9u80Hsqq1vCUn9pFJ0cxpdcnAv+QzU2zFH3Q==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true dependencies: - rollup: 3.29.4 + rollup: 4.0.2 slash: 4.0.0 dev: false - /@rollup/plugin-json@6.0.0(rollup@3.29.4): - resolution: {integrity: sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==} + /@rollup/plugin-json@6.0.1(rollup@4.0.2): + resolution: {integrity: sha512-RgVfl5hWMkxN1h/uZj8FVESvPuBJ/uf6ly6GTj0GONnkfoBN5KC0MSz+PN2OLDgYXMhtG0mWpTrkiOjoxAIevw==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.4(rollup@3.29.4) - rollup: 3.29.4 + '@rollup/pluginutils': 5.0.5(rollup@4.0.2) + rollup: 4.0.2 dev: false - /@rollup/plugin-replace@5.0.2(rollup@3.29.4): - resolution: {integrity: sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==} + /@rollup/plugin-replace@5.0.3(rollup@4.0.2): + resolution: {integrity: sha512-je7fu05B800IrMlWjb2wzJcdXzHYW46iTipfChnBDbIbDXhASZs27W1B58T2Yf45jZtJUONegpbce+9Ut2Ti/Q==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.4(rollup@3.29.4) + '@rollup/pluginutils': 5.0.5(rollup@4.0.2) magic-string: 0.27.0 - rollup: 3.29.4 + rollup: 4.0.2 dev: false - /@rollup/pluginutils@5.0.4(rollup@3.29.4): - resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==} + /@rollup/pluginutils@5.0.5(rollup@4.0.2): + resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true @@ -5241,9 +5241,93 @@ packages: '@types/estree': 1.0.2 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 3.29.4 + rollup: 4.0.2 + + /@rollup/rollup-android-arm-eabi@4.0.2: + resolution: {integrity: sha512-xDvk1pT4vaPU2BOLy0MqHMdYZyntqpaBf8RhBiezlqG9OjY8F50TyctHo8znigYKd+QCFhCmlmXHOL/LoaOl3w==} + cpu: [arm] + os: [android] + requiresBuild: true + optional: true - /@rushstack/node-core-library@3.61.0(@types/node@20.7.1): + /@rollup/rollup-android-arm64@4.0.2: + resolution: {integrity: sha512-lqCglytY3E6raze27DD9VQJWohbwCxzqs9aSHcj5X/8hJpzZfNdbsr4Ja9Hqp6iPyF53+5PtPx0pKRlkSvlHZg==} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true + + /@rollup/rollup-darwin-arm64@4.0.2: + resolution: {integrity: sha512-nkBKItS6E6CCzvRwgiKad+j+1ibmL7SIInj7oqMWmdkCjiSX6VeVZw2mLlRKIUL+JjsBgpATTfo7BiAXc1v0jA==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@rollup/rollup-darwin-x64@4.0.2: + resolution: {integrity: sha512-vX2C8xvWPIbpEgQht95+dY6BReKAvtDgPDGi0XN0kWJKkm4WdNmq5dnwscv/zxvi+n6jUTBhs6GtpkkWT4q8Gg==} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.0.2: + resolution: {integrity: sha512-DVFIfcHOjgmeHOAqji4xNz2wczt1Bmzy9MwBZKBa83SjBVO/i38VHDR+9ixo8QpBOiEagmNw12DucG+v55tCrg==} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.0.2: + resolution: {integrity: sha512-GCK/a9ItUxPI0V5hQEJjH4JtOJO90GF2Hja7TO+EZ8rmkGvEi8/ZDMhXmcuDpQT7/PWrTT9RvnG8snMd5SrhBQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.0.2: + resolution: {integrity: sha512-cLuBp7rOjIB1R2j/VazjCmHC7liWUur2e9mFflLJBAWCkrZ+X0+QwHLvOQakIwDymungzAKv6W9kHZnTp/Mqrg==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.0.2: + resolution: {integrity: sha512-Zqw4iVnJr2naoyQus0yLy7sLtisCQcpdMKUCeXPBjkJtpiflRime/TMojbnl8O3oxUAj92mxr+t7im/RbgA20w==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.0.2: + resolution: {integrity: sha512-jJRU9TyUD/iMqjf8aLAp7XiN3pIj5v6Qcu+cdzBfVTKDD0Fvua4oUoK8eVJ9ZuKBEQKt3WdlcwJXFkpmMLk6kg==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.0.2: + resolution: {integrity: sha512-ZkS2NixCxHKC4zbOnw64ztEGGDVIYP6nKkGBfOAxEPW71Sji9v8z3yaHNuae/JHPwXA+14oDefnOuVfxl59SmQ==} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.0.2: + resolution: {integrity: sha512-3SKjj+tvnZ0oZq2BKB+fI+DqYI83VrRzk7eed8tJkxeZ4zxJZcLSE8YDQLYGq1tZAnAX+H076RHHB4gTZXsQzw==} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.0.2: + resolution: {integrity: sha512-MBdJIOxRauKkry7t2q+rTHa3aWjVez2eioWg+etRVS3dE4tChhmt5oqZYr48R6bPmcwEhxQr96gVRfeQrLbqng==} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /@rushstack/node-core-library@3.61.0(@types/node@20.8.4): resolution: {integrity: sha512-tdOjdErme+/YOu4gPed3sFS72GhtWCgNV9oDsHDnoLY5oDfwjKUc9Z+JOZZ37uAxcm/OCahDHfuu2ugqrfWAVQ==} peerDependencies: '@types/node': '*' @@ -5251,7 +5335,7 @@ packages: '@types/node': optional: true dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 colors: 1.2.5 fs-extra: 7.0.1 import-lazy: 4.0.0 @@ -5806,8 +5890,8 @@ packages: resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} dev: false - /@storybook/addon-actions@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-FkjJWmPN/+duLSkRwfa2bwlwjKfY6yCXYn7CRzn3rb64B8f50NB79zAgVLHjkJh9l6T3DIlWtol6vqPHj1aRpw==} + /@storybook/addon-actions@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-SsqZr3js5NinKPnC8AeNI7Ij+Q6fIl9tRdRmSulEgjksjOg7E5S1/Wsn5Bb2CCgj7MaX6VxGyC7s3XskQtDiIQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5817,14 +5901,14 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 dequal: 2.0.3 lodash: 4.17.21 polished: 4.2.2 @@ -5840,8 +5924,8 @@ packages: - '@types/react-dom' dev: true - /@storybook/addon-backgrounds@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-fTq9E1WrYH/9hwDemFVLVcaI2iSSuwWnvY/8tqGrY9xhQF5dIpeHf+z8+HWXpau7e6Z0/WiYR+1vwAcIKt95LQ==} + /@storybook/addon-backgrounds@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-+LHTZB/ZYMAzkyD5ZxSriBsqmsrvIaW/Nnd/BeuXGbkrVKKqM0qAKiFZAfjc2WchA1piVNy0/1Rsf+kuYCEiJw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5851,14 +5935,14 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 memoizerific: 1.11.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5868,8 +5952,8 @@ packages: - '@types/react-dom' dev: true - /@storybook/addon-controls@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Mxs56jt44HIbZ4gJa0AII1U8GqEGFsvcM5Iob0ETNpxCW5Kj5iHly/4Ws0RFWPH/krrQKaLpWXaUxKmbtEzhJA==} + /@storybook/addon-controls@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-4lq3sycEUIsK8SUWDYc60QgF4vV9FZZ3lDr6M7j2W9bOnvGw49d2fbdlnq+bX1ZprZZ9VgglQpBAorQB3BXZRw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5879,16 +5963,16 @@ packages: react-dom: optional: true dependencies: - '@storybook/blocks': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-common': 7.4.5 - '@storybook/core-events': 7.4.5 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/node-logger': 7.4.5 - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/blocks': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-common': 7.4.6 + '@storybook/core-events': 7.4.6 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/node-logger': 7.4.6 + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 lodash: 4.17.21 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5900,27 +5984,27 @@ packages: - supports-color dev: true - /@storybook/addon-docs@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-KjFVeq8oL7ZC1gsk8iY3Nn0RrHHUpczmOTCd8FeVNmKD4vq+dkPb/8bJLy+jArmIZ8vRhknpTh6kp1BqB7qHGQ==} + /@storybook/addon-docs@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-dLaub+XWFq4hChw+xfuF9yYg0Txp77FUawKoAigccfjWXx+OOhRV3XTuAcknpXkYq94GWynHgUFXosXT9kbDNA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@jest/transform': 29.7.0 '@mdx-js/react': 2.3.0(react@18.2.0) - '@storybook/blocks': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/csf-plugin': 7.4.5 - '@storybook/csf-tools': 7.4.5 + '@storybook/blocks': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/csf-plugin': 7.4.6 + '@storybook/csf-tools': 7.4.6 '@storybook/global': 5.0.0 '@storybook/mdx2-csf': 1.0.0 - '@storybook/node-logger': 7.4.5 - '@storybook/postinstall': 7.4.5 - '@storybook/preview-api': 7.4.5 - '@storybook/react-dom-shim': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/node-logger': 7.4.6 + '@storybook/postinstall': 7.4.6 + '@storybook/preview-api': 7.4.6 + '@storybook/react-dom-shim': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 fs-extra: 11.1.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5934,25 +6018,25 @@ packages: - supports-color dev: true - /@storybook/addon-essentials@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-H7zZWJXZP0UU2kXfo9zlQfjIKHuuqYBK7PZ2/SL5y08mTrbtt1BfqYScz3xRvHocaFcsBWCXdy8jJULT4KFUpw==} + /@storybook/addon-essentials@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-dWodufrt71TK7ELkeIvVae/x4PzECUlbOm57Iqqt4yQCyR291CgvI4PjeB8un2HbpcXCGZ+N/Oj3YkytvzBi4A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/addon-actions': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-backgrounds': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-controls': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-docs': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-highlight': 7.4.5 - '@storybook/addon-measure': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-outline': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-toolbars': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-viewport': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-common': 7.4.5 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/node-logger': 7.4.5 - '@storybook/preview-api': 7.4.5 + '@storybook/addon-actions': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-backgrounds': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-controls': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-docs': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-highlight': 7.4.6 + '@storybook/addon-measure': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-outline': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-toolbars': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-viewport': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-common': 7.4.6 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/node-logger': 7.4.6 + '@storybook/preview-api': 7.4.6 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) ts-dedent: 2.2.0 @@ -5963,16 +6047,16 @@ packages: - supports-color dev: true - /@storybook/addon-highlight@7.4.5: - resolution: {integrity: sha512-6Ru411+Iis4m2weKb8kB1eEssLvCHwFqAf4fjcOC//O5Vaf5+beHYZJUm/rzD0k/oUHfLCBwDBSBY5TLRegkdA==} + /@storybook/addon-highlight@7.4.6: + resolution: {integrity: sha512-zCufxxD2KS5VwczxfkcBxe1oR/juTTn2H1Qm8kYvWCJQx3UxzX0+G9cwafbpV7eivqaufLweEwROkH+0KjAtkQ==} dependencies: - '@storybook/core-events': 7.4.5 + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.4.5 + '@storybook/preview-api': 7.4.6 dev: true - /@storybook/addon-interactions@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-KDdV/THxj38VsuOevrUefev0rZPhzqUXCgrw1Jc2PsJGidHf9d9nnB7wbA9ZFYsxTz90M/Vk5sm7i1QkMmsquA==} + /@storybook/addon-interactions@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-zVZYrEPZPhNrXBuPqM7HbQvr6jwsje1sbCYj3wnp83U5wjciuqrngqHIlaSZ30zOWSfRVyzbyqL+JQZKA58BNA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5982,16 +6066,16 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-common': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-common': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/instrumenter': 7.4.5 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/instrumenter': 7.4.6 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 jest-mock: 27.5.1 polished: 4.2.2 react: 18.2.0 @@ -6004,8 +6088,8 @@ packages: - supports-color dev: true - /@storybook/addon-links@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-eKczq3U5KfPLaxMUzzVQQrGVtzDshUmrSEEuWKf9ZbK3mh5yVuagIBb88edgUX58vZ3TJMvqQzq1+BtUoPHQ6Q==} + /@storybook/addon-links@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BPygElZKX+CPI9Se6GJNk1dYc5oxuhA+vHigO1tBqhiM6VkHyFP3cvezJNQvpNYhkUnu3cxnZXb3UJnlRbPY3g==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6015,22 +6099,22 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/router': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/router': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) ts-dedent: 2.2.0 dev: true - /@storybook/addon-measure@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-FQGZniTH67nC1YPR4ep0p+isgxwLaNAmIAyCZWXPRTkZssIrnXVwNgi0A2QkHdxZvxj8yXGFTOVXLWEPT9YvFQ==} + /@storybook/addon-measure@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-nCymMLaHnxv8TE3yEM1A9Tulb1NuRXRNmtsdHTkjv7P1aWCxZo8A/GZaottKe/GLT8jSRjZ+dnpYWrbAhw6wTQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6040,13 +6124,13 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/types': 7.4.6 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) tiny-invariant: 1.3.1 @@ -6055,8 +6139,8 @@ packages: - '@types/react-dom' dev: true - /@storybook/addon-outline@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-eOH9BZzpehUz5FXD98OLnWgzmBFMvEB2kFfw5JiO7IRx7Fan80fx/WDQuMSNDOgLBCTTvsZ4TBMMXZHpw91WAw==} + /@storybook/addon-outline@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-errNUblRVDLpuEaHQPr/nsrnsUkD2ARmXawkRvizgDWLIDMDJYjTON3MUCaVx3x+hlZ3I6X//G5TVcma8tCc8A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6066,13 +6150,13 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/types': 7.4.6 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) ts-dedent: 2.2.0 @@ -6081,8 +6165,8 @@ packages: - '@types/react-dom' dev: true - /@storybook/addon-storysource@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-aWQkW4IzDHRXdUyHPfksSdk4zK4gIJvXpxVCqX+oz3FuadmwZmhK1vWxNdm4Jo/0EZdwe2YZOBJwXHIwpZtigg==} + /@storybook/addon-storysource@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-qkfwvh/pgVBReuWqO25WyaD7jd6LVqhoIJ6rBWnmx+NBpTds+h3Yt3UJCHgvweIrfSF8J3IqzaTxmmNjnkcrRw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6092,13 +6176,13 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/router': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/source-loader': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/router': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/source-loader': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) estraverse: 5.3.0 prop-types: 15.8.1 react: 18.2.0 @@ -6110,8 +6194,8 @@ packages: - '@types/react-dom' dev: true - /@storybook/addon-toolbars@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-PZlwUTIdQ18de3zNb+627VSF4UrCGIXDdikyO9O5j2Cd0xfr5uhS6tgQ+3AT0DfUj0UIkKxilwcAt+agpNyicA==} + /@storybook/addon-toolbars@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-L9m2FBcKeteGq7qIYsMJr0LEfiH7Wdrv5IDcldZTn68eZUJTh1p4GdJZcOmzX1P5IFRr76hpu03iWsNlWQjpbQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6121,11 +6205,11 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -6133,8 +6217,8 @@ packages: - '@types/react-dom' dev: true - /@storybook/addon-viewport@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-SBLnUMIztVrqJ0fRCsVg9KZ29APLIxqAvTsYHF3twy5KB2naeCFuX3K9LxSH7vbROI6zHEfnPduz/Ykyvu9yUg==} + /@storybook/addon-viewport@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-INDtk54j7bi7NgxMfd2ATmbA0J7nAd6X8itMkLIyPuPJtx8bYHPDORyemDOd0AojgmAdTOAyUtDYdI/PFeo4Cw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6144,13 +6228,13 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) memoizerific: 1.11.3 prop-types: 15.8.1 react: 18.2.0 @@ -6160,36 +6244,36 @@ packages: - '@types/react-dom' dev: true - /@storybook/addons@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-jmdQf39XhwVi8d0J99qpk51fOAwNhYlCtVctvFWPX4qC1cq1d1pxLmTb5OBV2VHQ11BKwlKLzA7coiOgAQmNRg==} + /@storybook/addons@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-c+4awrtwNlJayFdgLkEXa5H2Gj+KNlxuN+Z5oDAdZBLqXI8g0gn7eYO2F/eCSIDWdd/+zcU2uq57XPFKc8veHQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/types': 7.4.6 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/blocks@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-FhAIkCT2HrzJcKsC3mL5+uG3GrbS23mYAT1h3iyPjCliZzxfCCI9UCMUXqYx4Z/FmAGJgpsQQXiBFZuoTHO9aQ==} + /@storybook/blocks@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HxBSAeOiTZW2jbHQlo1upRWFgoMsaAyKijUFf5MwwMNIesXCuuTGZDJ3xTABwAVLK2qC9Ektfbo0CZCiPVuDRQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/channels': 7.4.5 - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 + '@storybook/channels': 7.4.6 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 '@storybook/csf': 0.1.0 - '@storybook/docs-tools': 7.4.5 + '@storybook/docs-tools': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 '@types/lodash': 4.14.191 color-convert: 2.0.1 dequal: 2.0.3 @@ -6211,13 +6295,13 @@ packages: - supports-color dev: true - /@storybook/builder-manager@7.4.5: - resolution: {integrity: sha512-Jhql8iZgK9cxDmG9NSTejsj5FptHni2TBa5Sea2Uz1NIBQ0OpzNdUfYVX6TN/PEq3QrWXTrAEKPqsL2qGjOrxw==} + /@storybook/builder-manager@7.4.6: + resolution: {integrity: sha512-zylZCD2rmyLOOFBFmUgtJg6UNUKmRNgXiig1XApzS2TkIbTZP827DsVEUl0ey/lskCe0uArkrEBR6ICba8p/Rw==} dependencies: '@fal-works/esbuild-plugin-global-externals': 2.1.2 - '@storybook/core-common': 7.4.5 - '@storybook/manager': 7.4.5 - '@storybook/node-logger': 7.4.5 + '@storybook/core-common': 7.4.6 + '@storybook/manager': 7.4.6 + '@storybook/node-logger': 7.4.6 '@types/ejs': 3.1.2 '@types/find-cache-dir': 3.2.1 '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.18.17) @@ -6235,8 +6319,8 @@ packages: - supports-color dev: true - /@storybook/builder-vite@7.4.5(typescript@5.2.2)(vite@4.4.9): - resolution: {integrity: sha512-0aIMvBIx2U/DhDjdjWCW/KIG3HAJpus8NIUIvkVAUCaA7Vn8XvnSsdaRSTTxaaJReFZcIxDf7MebHSCJ0UEKqQ==} + /@storybook/builder-vite@7.4.6(typescript@5.2.2)(vite@4.4.11): + resolution: {integrity: sha512-xV9STYK+TkqWWTf2ydm6jx+7P70fjD2UPd1XTUw08uKszIjhuuxk+bG/OF5R1E25mPunAKXm6kBFh351AKejBg==} peerDependencies: '@preact/preset-vite': '*' typescript: '>= 4.3.x' @@ -6250,15 +6334,15 @@ packages: vite-plugin-glimmerx: optional: true dependencies: - '@storybook/channels': 7.4.5 - '@storybook/client-logger': 7.4.5 - '@storybook/core-common': 7.4.5 - '@storybook/csf-plugin': 7.4.5 + '@storybook/channels': 7.4.6 + '@storybook/client-logger': 7.4.6 + '@storybook/core-common': 7.4.6 + '@storybook/csf-plugin': 7.4.6 '@storybook/mdx2-csf': 1.0.0 - '@storybook/node-logger': 7.4.5 - '@storybook/preview': 7.4.5 - '@storybook/preview-api': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/node-logger': 7.4.6 + '@storybook/preview': 7.4.6 + '@storybook/preview-api': 7.4.6 + '@storybook/types': 7.4.6 '@types/find-cache-dir': 3.2.1 browser-assert: 1.2.1 es-module-lexer: 0.9.3 @@ -6270,50 +6354,39 @@ packages: remark-slug: 6.1.0 rollup: 3.29.4 typescript: 5.2.2 - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.4)(sass@1.69.1)(terser@5.21.0) transitivePeerDependencies: - encoding - supports-color dev: true - /@storybook/channels@7.4.4: - resolution: {integrity: sha512-YA2T3hClL95nFBBelm8wMOyWFDzfxKvyHAPQi+8YeYpZcPivwg/P9YnRhTTMbiZNkfoWKq4ZRuc79UP1iNLi3g==} - dependencies: - '@storybook/client-logger': 7.4.4 - '@storybook/core-events': 7.4.4 - '@storybook/global': 5.0.0 - qs: 6.11.1 - telejson: 7.2.0 - tiny-invariant: 1.3.1 - dev: true - - /@storybook/channels@7.4.5: - resolution: {integrity: sha512-zWPZn4CxPFXsrrSRQ9JD8GmTeWeFYgr3sTBpe23hnhYookCXVNJ6AcaXogrT9b2ALfbB6MiFDbZIHHTgIgbWpg==} + /@storybook/channels@7.4.6: + resolution: {integrity: sha512-yPv/sfo2c18fM3fvG0i1xse63vG8l33Al/OU0k/dtovltPu001/HVa1QgBgsb/QrEfZtvGjGhmtdVeYb39fv3A==} dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 qs: 6.11.1 telejson: 7.2.0 tiny-invariant: 1.3.1 dev: true - /@storybook/cli@7.4.5: - resolution: {integrity: sha512-PlTkcHdKCugg3pD1zkBP/oFazcZsr7F3wdEmTvygfH0Cx/sQWg5wXBZCYKmf0ONRK4RKL3LVM8DRpeYiQVEFWg==} + /@storybook/cli@7.4.6: + resolution: {integrity: sha512-rRwaH8pOL+FHz/pJMEkNpMH2xvZvWsrl7obBYw26NQiHmiVSAkfHJicndSN1mwc+p5w+9iXthrgzbLtSAOSvkA==} hasBin: true dependencies: '@babel/core': 7.22.11 '@babel/preset-env': 7.22.9(@babel/core@7.22.11) '@babel/types': 7.22.17 '@ndelangen/get-tarball': 3.0.7 - '@storybook/codemod': 7.4.5 - '@storybook/core-common': 7.4.5 - '@storybook/core-events': 7.4.5 - '@storybook/core-server': 7.4.5 - '@storybook/csf-tools': 7.4.5 - '@storybook/node-logger': 7.4.5 - '@storybook/telemetry': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/codemod': 7.4.6 + '@storybook/core-common': 7.4.6 + '@storybook/core-events': 7.4.6 + '@storybook/core-server': 7.4.6 + '@storybook/csf-tools': 7.4.6 + '@storybook/node-logger': 7.4.6 + '@storybook/telemetry': 7.4.6 + '@storybook/types': 7.4.6 '@types/semver': 7.5.3 '@yarnpkg/fslib': 2.10.3 '@yarnpkg/libzip': 2.3.0 @@ -6350,28 +6423,22 @@ packages: - utf-8-validate dev: true - /@storybook/client-logger@7.4.4: - resolution: {integrity: sha512-rC/GcCy3DLtTI+oOHLBc6rq/c3oGF/mvdeWrhMM+berQplHJrOCI2pcldjVw8Fc25gLPK0LUlaOp1dfgt2Ri3Q==} - dependencies: - '@storybook/global': 5.0.0 - dev: true - - /@storybook/client-logger@7.4.5: - resolution: {integrity: sha512-Bn6eTAjhPDUfLpvuxhKkpDpOtkadfkSmkBNBZRu3r0Dzk2J1nNyKV5K6D8dOU4PFVof4z/gXYj5bktT29jKsmw==} + /@storybook/client-logger@7.4.6: + resolution: {integrity: sha512-XDw31ZziU//86PKuMRnmc+L/G0VopaGKENQOGEpvAXCU9IZASwGKlKAtcyosjrpi+ZiUXlMgUXCpXM7x3b1Ehw==} dependencies: '@storybook/global': 5.0.0 dev: true - /@storybook/codemod@7.4.5: - resolution: {integrity: sha512-gyI2xliSv4vvnfNQN+0e3tRmT7beiq8q8iGjcBtpOhA2xrStyCR7PjbOfLXtRx2I/b50MDZMRTcckzeM3BLoWQ==} + /@storybook/codemod@7.4.6: + resolution: {integrity: sha512-lxmwEpwksCaAq96APN2YlooSDfKjJ1vKzN5Ni2EqQzf2TEXl7XQjLacHd7OOaII1kfsy+D5gNG4N5wBo7Ub30g==} dependencies: '@babel/core': 7.22.11 '@babel/preset-env': 7.22.9(@babel/core@7.22.11) '@babel/types': 7.22.17 '@storybook/csf': 0.1.0 - '@storybook/csf-tools': 7.4.5 - '@storybook/node-logger': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/csf-tools': 7.4.6 + '@storybook/node-logger': 7.4.6 + '@storybook/types': 7.4.6 '@types/cross-spawn': 6.0.2 cross-spawn: 7.0.3 globby: 11.1.0 @@ -6383,42 +6450,19 @@ packages: - supports-color dev: true - /@storybook/components@7.4.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-tFOSu3IoAab/0aY2TY66Go0Nba7AB/+ZB9GFet+dxWypIKGLcPjyX2POIumJU4swzK+4IA8GxgDQ2itS6EOISQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@radix-ui/react-select': 1.2.2(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-toolbar': 1.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 7.4.4 - '@storybook/csf': 0.1.0 - '@storybook/global': 5.0.0 - '@storybook/theming': 7.4.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.4 - memoizerific: 1.11.3 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - use-resize-observer: 9.1.0(react-dom@18.2.0)(react@18.2.0) - util-deprecate: 1.0.2 - transitivePeerDependencies: - - '@types/react' - - '@types/react-dom' - dev: true - - /@storybook/components@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-boskkfvMBB8CFYY9+1ofFNyKrdWXTY/ghzt7oK80dz6f2Eseo/WXK3OsCdCq5vWbLRCdbgJ8zXG8pAFi4yBsxA==} + /@storybook/components@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-nIRBhewAgrJJVafyCzuaLx1l+YOfvvD5dOZ0JxZsxJsefOdw1jFpUqUZ5fIpQ2moyvrR0mAUFw378rBfMdHz5Q==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@radix-ui/react-select': 1.2.2(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-toolbar': 1.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 7.4.5 + '@storybook/client-logger': 7.4.6 '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 memoizerific: 1.11.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -6429,19 +6473,19 @@ packages: - '@types/react-dom' dev: true - /@storybook/core-client@7.4.5: - resolution: {integrity: sha512-d/qiCUZeOKY0HX/YmomxlccxJ2NKC3ttRrAsAXzJGypClKabv20X+qbeO/E7Kp5UQxIEJx1wuwJPcnlCvjgPDA==} + /@storybook/core-client@7.4.6: + resolution: {integrity: sha512-tfgxAHeCvMcs6DsVgtb4hQSDaCHeAPJOsoyhb47eDQfk4OmxzriM0qWucJV5DePSMi+KutX/rN2u0JxfOuN68g==} dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/preview-api': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/preview-api': 7.4.6 dev: true - /@storybook/core-common@7.4.5: - resolution: {integrity: sha512-c4pBuILMD4YhSpJ+QpKtsUZpK+/rfolwOvzXfJwlN5EpYzMz6FjVR/LyX0cCT2YLI3X5YWRoCdvMxy5Aeryb8g==} + /@storybook/core-common@7.4.6: + resolution: {integrity: sha512-05MJFmOM86qvTLtgDskokIFz9txe0Lbhq4L3by1FtF0GwgH+p+W6I94KI7c6ANER+kVZkXQZhiRzwBFnVTW+Cg==} dependencies: - '@storybook/core-events': 7.4.5 - '@storybook/node-logger': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/core-events': 7.4.6 + '@storybook/node-logger': 7.4.6 + '@storybook/types': 7.4.6 '@types/find-cache-dir': 3.2.1 '@types/node': 16.18.46 '@types/node-fetch': 2.6.4 @@ -6467,36 +6511,30 @@ packages: - supports-color dev: true - /@storybook/core-events@7.4.4: - resolution: {integrity: sha512-kOf4I/a1XC9CaGFwJG5WR2KnkwrOkWX68TLh7OlelKxdl/WjxA4zfzaFPC/8zyCSLdGFLPKNqr1w+ezkb+9Irw==} + /@storybook/core-events@7.4.6: + resolution: {integrity: sha512-r5vrE+32lwrJh1NGFr1a0mWjvxo7q8FXYShylcwRWpacmL5NTtLkrXOoJSeGvJ4yKNYkvxQFtOPId4lzDxa32w==} dependencies: ts-dedent: 2.2.0 dev: true - /@storybook/core-events@7.4.5: - resolution: {integrity: sha512-Jzy/adSC95saYCZlgXE5j7jmiMLAXYpnBFBxEtBdXwSWEBb0zt21n1nyWBEAv9s/k2gqDXlPHKHeL5Mn6y40zA==} - dependencies: - ts-dedent: 2.2.0 - dev: true - - /@storybook/core-server@7.4.5: - resolution: {integrity: sha512-cW+Qx9Ls823577bd/s9Kv4M1MdKS8mkk6/+nYbwtAwH3hkdlb077rlk/ue0X4O9NZmCrtaJ84KNrBkeDUdFyLQ==} + /@storybook/core-server@7.4.6: + resolution: {integrity: sha512-jqmRTGCJ1W0WReImivkisPVaLFT5sjtLnFoAk0feHp6QS5j7EYOPN7CYzliyQmARWTLUEXOVaFf3VD6nJZQhJQ==} dependencies: '@aw-web-design/x-default-browser': 1.4.126 '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-manager': 7.4.5 - '@storybook/channels': 7.4.5 - '@storybook/core-common': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/builder-manager': 7.4.6 + '@storybook/channels': 7.4.6 + '@storybook/core-common': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/csf': 0.1.0 - '@storybook/csf-tools': 7.4.5 + '@storybook/csf-tools': 7.4.6 '@storybook/docs-mdx': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/manager': 7.4.5 - '@storybook/node-logger': 7.4.5 - '@storybook/preview-api': 7.4.5 - '@storybook/telemetry': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/manager': 7.4.6 + '@storybook/node-logger': 7.4.6 + '@storybook/preview-api': 7.4.6 + '@storybook/telemetry': 7.4.6 + '@storybook/types': 7.4.6 '@types/detect-port': 1.3.2 '@types/node': 16.18.46 '@types/pretty-hrtime': 1.0.1 @@ -6516,7 +6554,6 @@ packages: prompts: 2.4.2 read-pkg-up: 7.0.1 semver: 7.5.4 - serve-favicon: 2.5.0 telejson: 7.2.0 tiny-invariant: 1.3.1 ts-dedent: 2.2.0 @@ -6531,24 +6568,24 @@ packages: - utf-8-validate dev: true - /@storybook/csf-plugin@7.4.5: - resolution: {integrity: sha512-8p3AnwIm3xXtQhiF7OQ0rBiP/Pn5OCMHRiT4FytRnNimGaw7gxRZ2xzM608QZHQ4A8rHfmgoM2FTwgxdC15ulA==} + /@storybook/csf-plugin@7.4.6: + resolution: {integrity: sha512-yi7Qa4NSqKOyiJTWCxlB0ih2ijXq6oY5qZKW6MuMMBP14xJNRGLbH5KabpfXgN2T7YECcOWG1uWaGj2veJb1KA==} dependencies: - '@storybook/csf-tools': 7.4.5 + '@storybook/csf-tools': 7.4.6 unplugin: 1.4.0 transitivePeerDependencies: - supports-color dev: true - /@storybook/csf-tools@7.4.5: - resolution: {integrity: sha512-xbm5HGYvlwF0Efivx37v9rO7Exel1/Tdb/Yv/vXn4D/hQeljNVLNz4Bomfy4EQ207rRsrGDSOHEhLUbHDimnxg==} + /@storybook/csf-tools@7.4.6: + resolution: {integrity: sha512-ocKpcIUtTBy6hlLY34RUFQyX403cWpB2gGfqvkHbpGe2BQj7EyV0zpWnjsfVxvw+M9OWlCdxHWDOPUgXM33ELw==} dependencies: '@babel/generator': 7.22.10 '@babel/parser': 7.22.16 '@babel/traverse': 7.22.11 '@babel/types': 7.22.17 '@storybook/csf': 0.1.0 - '@storybook/types': 7.4.5 + '@storybook/types': 7.4.6 fs-extra: 11.1.1 recast: 0.23.1 ts-dedent: 2.2.0 @@ -6566,12 +6603,12 @@ packages: resolution: {integrity: sha512-JDaBR9lwVY4eSH5W8EGHrhODjygPd6QImRbwjAuJNEnY0Vw4ie3bPkeGfnacB3OBW6u/agqPv2aRlR46JcAQLg==} dev: true - /@storybook/docs-tools@7.4.5: - resolution: {integrity: sha512-ctK+yGb2nvWISSvCCzj3ZhDaAb7I2BLjbxuBGTyNPvl4V9UQ9LBYzdJwR50q+DfscxdwSHMSOE/0OnzmJdaSJA==} + /@storybook/docs-tools@7.4.6: + resolution: {integrity: sha512-nZj1L/8WwKWWJ41FW4MaKGajZUtrhnr9UwflRCkQJaWhAKmDfOb5M5TqI93uCOULpFPOm5wpoMBz2IHInQ2Lrg==} dependencies: - '@storybook/core-common': 7.4.5 - '@storybook/preview-api': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/core-common': 7.4.6 + '@storybook/preview-api': 7.4.6 + '@storybook/types': 7.4.6 '@types/doctrine': 0.0.3 doctrine: 3.0.0 lodash: 4.17.21 @@ -6590,21 +6627,21 @@ packages: resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} dev: true - /@storybook/instrumenter@7.4.5: - resolution: {integrity: sha512-VLFOcmG75QhWa7MtmfEybIJEz5oT2Ry8xAy/pIVhQwyBaeW0kRT0MHWkixRTtWQmJs/78FmHE3FlgMnqpa5JoA==} + /@storybook/instrumenter@7.4.6: + resolution: {integrity: sha512-K5atRoVFCl6HEgkSxIbwygpzgE/iROc7BrtJ3z3a7E70sanFr6Jxt6Egu6fz2QkL3ef4EWpXMnle2vhEfG29pA==} dependencies: - '@storybook/channels': 7.4.5 - '@storybook/client-logger': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/channels': 7.4.6 + '@storybook/client-logger': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.4.5 + '@storybook/preview-api': 7.4.6 dev: true - /@storybook/jest@0.2.2(vitest@0.34.5): - resolution: {integrity: sha512-PUfp9WoqUA8NdAmiz3UahUsyAMr+g1Dv3BB0fqJZsE2IuE5o1Mgsv4iLGzFm+ohcQLIDQvwvvbQIpxe8eY7TNw==} + /@storybook/jest@0.2.3(vitest@0.34.6): + resolution: {integrity: sha512-ov5izrmbAFObzKeh9AOC5MlmFxAcf0o5i6YFGae9sDx6DGh6alXsRM+chIbucVkUwVHVlSzdfbLDEFGY/ShaYw==} dependencies: '@storybook/expect': 28.1.3-5 - '@testing-library/jest-dom': 6.1.2(@types/jest@28.1.3)(vitest@0.34.5) + '@testing-library/jest-dom': 6.1.2(@types/jest@28.1.3)(vitest@0.34.6) '@types/jest': 28.1.3 jest-mock: 27.5.1 transitivePeerDependencies: @@ -6613,20 +6650,20 @@ packages: - vitest dev: true - /@storybook/manager-api@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-8Hdh5Tutet8xRy2fAknczfvpshz09eVnLd8m34vcFceUOYvEnvDbWerufhlEzovsF4v7U32uqbDHKdKTamWEQQ==} + /@storybook/manager-api@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-inrm3DIbCp8wjXSN/wK6e6i2ysQ/IEmtC7IN0OJ7vdrp+USCooPT448SQTUmVctUGCFmOU3fxXByq8g77oIi7w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/channels': 7.4.5 - '@storybook/client-logger': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/channels': 7.4.6 + '@storybook/client-logger': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/router': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/router': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 @@ -6638,31 +6675,31 @@ packages: ts-dedent: 2.2.0 dev: true - /@storybook/manager@7.4.5: - resolution: {integrity: sha512-yoqVktWzzC0f8cXsxErOEUfT+VFfWV/W7soytIPQuJFqNaq+BqR5A7WCeoY7BIv3mdpRjo4GKwerCsgoHYeHhg==} + /@storybook/manager@7.4.6: + resolution: {integrity: sha512-kA1hUDxpn1i2SO9OinvLvVXDeL4xgJkModp+pbE8IXv4NJWReNq1ecMeQCzPLS3Sil2gnrullQ9uYXsnZ9bxxA==} dev: true /@storybook/mdx2-csf@1.0.0: resolution: {integrity: sha512-dBAnEL4HfxxJmv7LdEYUoZlQbWj9APZNIbOaq0tgF8XkxiIbzqvgB0jhL/9UOrysSDbQWBiCRTu2wOVxedGfmw==} dev: true - /@storybook/node-logger@7.4.5: - resolution: {integrity: sha512-fJSykphbryuEYj1qihbaTH5oOzD4NkptRxyf2uyBrpgkr5tCTq9d7GHheqaBuIdi513dsjlcIR7z5iHxW7ZD+Q==} + /@storybook/node-logger@7.4.6: + resolution: {integrity: sha512-djZb310Q27GviDug1XBv0jOEDLCiwr4hhDE0aifCEKZpfNCi/EaP31nbWimFzZwxu4hE/YAPWExzScruR1zw9Q==} dev: true - /@storybook/postinstall@7.4.5: - resolution: {integrity: sha512-MWRjnKkUpEe2VkHNNpv3zkuMvxM2Zu9DMxFENQaEmhqUHkIFh5klfFwzhSBRexVLzIh7DA1p7mntIpY5A6lh+Q==} + /@storybook/postinstall@7.4.6: + resolution: {integrity: sha512-TqI5BucPAGRWrkh55BYiG2/gHLFtC0In4cuu0GsUzB/1jc4i51npLRorCwhmT7r7YliGl5F7JaP0Bni/qHN3Lg==} dev: true - /@storybook/preview-api@7.4.5: - resolution: {integrity: sha512-6xXQZPyilkGVddfZBI7tMbMMgOyIoZTYgTnwSPTMsXxO0f0TvtNDmGdwhn0I1nREHKfiQGpcQe6gwddEMnGtSg==} + /@storybook/preview-api@7.4.6: + resolution: {integrity: sha512-byUS/Opt3ytWD4cWz3sNEKw5Yks8MkQgRN+GDSyIomaEAQkLAM0rchPC0MYjwCeUSecV7IIQweNX5RbV4a34BA==} dependencies: - '@storybook/channels': 7.4.5 - '@storybook/client-logger': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/channels': 7.4.6 + '@storybook/client-logger': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/types': 7.4.5 + '@storybook/types': 7.4.6 '@types/qs': 6.9.7 dequal: 2.0.3 lodash: 4.17.21 @@ -6673,12 +6710,12 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/preview@7.4.5: - resolution: {integrity: sha512-hCVFoPJP0d7vFCJKaWEsDMa6LcRFcEikQ8Cy6Vo+trS8xXwvwE+vIBqyuPozl4O/MYD9iOlzjgZFNwaUUgX0Jg==} + /@storybook/preview@7.4.6: + resolution: {integrity: sha512-2RPXusJ4CTDrIipIKKvbotD7fP0+8VzoFjImunflIrzN9rni+2rq5eMjqlXAaB+77w064zIR4uDUzI9fxsMDeQ==} dev: true - /@storybook/react-dom-shim@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-/hGe8yuiWbT7L3ZsllmJPgxT9MEQE3k23FhliyKx6IGHsWoYaEsPYPZ9tygqtKY8RpqqMUKWz8+kbO79zUxaoQ==} + /@storybook/react-dom-shim@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-DSq8l9FDocUF1ooVI+TF83pddj1LynE/Hv0/y8XZhc3IgJ/HkuOQuUmfz29ezgfAi9gFYUR8raTIBi3/xdoRmw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6687,25 +6724,25 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/react-vite@7.4.5(react-dom@18.2.0)(react@18.2.0)(rollup@3.29.4)(typescript@5.2.2)(vite@4.4.9): - resolution: {integrity: sha512-VfEktqZlSiAcM0oqUnXvQDIFM/G3pOZSW9VCcdQp2NWbsG/UVH42++ZkT0qJmQtW+Kkr8mTofLK5H1v5si5Z1A==} + /@storybook/react-vite@7.4.6(react-dom@18.2.0)(react@18.2.0)(rollup@4.0.2)(typescript@5.2.2)(vite@4.4.11): + resolution: {integrity: sha512-jkjnrf3FxzR5wcmebXRPflrsM4WIDjWyW/NVFJwxi5PeIOk7fE7/QAPrm4NFRUu2Q7DeuH3oLKsw8bigvUI9RA==} engines: {node: '>=16'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 vite: ^3.0.0 || ^4.0.0 dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.2.1(typescript@5.2.2)(vite@4.4.9) - '@rollup/pluginutils': 5.0.4(rollup@3.29.4) - '@storybook/builder-vite': 7.4.5(typescript@5.2.2)(vite@4.4.9) - '@storybook/react': 7.4.5(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) - '@vitejs/plugin-react': 3.1.0(vite@4.4.9) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.2.1(typescript@5.2.2)(vite@4.4.11) + '@rollup/pluginutils': 5.0.5(rollup@4.0.2) + '@storybook/builder-vite': 7.4.6(typescript@5.2.2)(vite@4.4.11) + '@storybook/react': 7.4.6(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) + '@vitejs/plugin-react': 3.1.0(vite@4.4.11) ast-types: 0.14.2 magic-string: 0.30.3 react: 18.2.0 react-docgen: 6.0.0-alpha.3 react-dom: 18.2.0(react@18.2.0) - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.4)(sass@1.69.1)(terser@5.21.0) transitivePeerDependencies: - '@preact/preset-vite' - encoding @@ -6715,8 +6752,8 @@ packages: - vite-plugin-glimmerx dev: true - /@storybook/react@7.4.5(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2): - resolution: {integrity: sha512-Tiylrs3uFO8QSvH1w3ueSxlAgh2fteH0edRVKaX01M/h47+QqEiZqq/dYkVDvLHngF+CCCwE3OY8nNe6L14Xkw==} + /@storybook/react@7.4.6(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2): + resolution: {integrity: sha512-w0dVo64baFFPTGpUOWFqkKsu6pQincoymegSNgqaBd5DxEyMDRiRoTWSJHMKE9BwgE8SyWhRkP1ak1mkccSOhQ==} engines: {node: '>=16.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6726,13 +6763,13 @@ packages: typescript: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/core-client': 7.4.5 - '@storybook/docs-tools': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/core-client': 7.4.6 + '@storybook/docs-tools': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.4.5 - '@storybook/react-dom-shim': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/preview-api': 7.4.6 + '@storybook/react-dom-shim': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 '@types/node': 16.18.46 @@ -6755,27 +6792,27 @@ packages: - supports-color dev: true - /@storybook/router@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-IM4IhiPiXsx3FAUeUOAB47uiuUS8Yd37VQcNlXLBO28GgHoTSYOrjS+VTGLIV5cAGKr8+H5pFB+q35BnlFUpkQ==} + /@storybook/router@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Vl1esrHkcHxDKqc+HY7+6JQpBPW3zYvGk0cQ2rxVMhWdLZTAz1hss9DqzN9tFnPyfn0a1Q77EpMySkUrvWKKNQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/client-logger': 7.4.5 + '@storybook/client-logger': 7.4.6 memoizerific: 1.11.3 qs: 6.11.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/source-loader@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-ieo/aPgIXAJfg2raDtsboX43IXiXYHDm0MSXvNXoFE7F1jtRe7gXRi8z7O9xTX4hlIuYea0+kHe+198adgLlWA==} + /@storybook/source-loader@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-tBso55luaKIsZmIsgYyT7HJcjbgjxf0pdzbYqdThZhY3oSl3d56xbcFDCWW+yWjFONuFY8RGPCT7iGywwmaBdQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@storybook/csf': 0.1.0 - '@storybook/types': 7.4.5 + '@storybook/types': 7.4.6 estraverse: 5.3.0 lodash: 4.17.21 prettier: 2.8.8 @@ -6783,12 +6820,12 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/telemetry@7.4.5: - resolution: {integrity: sha512-JbhQXZF5sqS2c7Cf+vAtuKTdTSBDco+liUP2UGQFjqdacTRLVzxyj+YY2UH4aAQn7wpmnQ67iHnqFp0+fdYmAA==} + /@storybook/telemetry@7.4.6: + resolution: {integrity: sha512-c8p/C1NIH8EMBviZkBCx8MMDk6rrITJ+b29DEp5MaWSRlklIVyhGiC4RPIRv6sxJwlD41PnqWVFtfu2j2eXLdQ==} dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/core-common': 7.4.5 - '@storybook/csf-tools': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/core-common': 7.4.6 + '@storybook/csf-tools': 7.4.6 chalk: 4.1.2 detect-package-manager: 2.0.1 fetch-retry: 5.0.4 @@ -6799,76 +6836,53 @@ packages: - supports-color dev: true - /@storybook/testing-library@0.2.1: - resolution: {integrity: sha512-AdbfLCm1C2nEFrhA3ScdicfW6Fjcorehr6RlGwECMiWwaXisnP971Wd4psqtWxlAqQo4tYBZ0f6rJ3J78JLtsg==} + /@storybook/testing-library@0.2.2: + resolution: {integrity: sha512-L8sXFJUHmrlyU2BsWWZGuAjv39Jl1uAqUHdxmN42JY15M4+XCMjGlArdCCjDe1wpTSW6USYISA9axjZojgtvnw==} dependencies: '@testing-library/dom': 9.2.0 '@testing-library/user-event': 14.4.3(@testing-library/dom@9.2.0) ts-dedent: 2.2.0 dev: true - /@storybook/theming@7.4.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-ABIwLRUj2IZKMGxKq+fCCFcY7w52P1a+q8j7qrlELaTe4M74K6rwTgRF0/AFgWeiGRkNuA7z8DjQ73xQLoLqUg==} + /@storybook/theming@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@emotion/use-insertion-effect-with-fallbacks': 1.0.0(react@18.2.0) - '@storybook/client-logger': 7.4.4 + '@storybook/client-logger': 7.4.6 '@storybook/global': 5.0.0 memoizerific: 1.11.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/theming@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-QSIJDIMzOegzlhubIBaYIovf4mlf+AVL0SmQOskPS8GZ6s9t77yUUI6gZTEjO+S4eB3djXRsfTTijQ8+z4XmRA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + /@storybook/types@7.4.6: + resolution: {integrity: sha512-6QLXtMVsFZFpzPkdGWsu/iuc8na9dnS67AMOBKm5qCLPwtUJOYkwhMdFRSSeJthLRpzV7JLAL8Kwvl7MFP3QSw==} dependencies: - '@emotion/use-insertion-effect-with-fallbacks': 1.0.0(react@18.2.0) - '@storybook/client-logger': 7.4.5 - '@storybook/global': 5.0.0 - memoizerific: 1.11.3 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: true - - /@storybook/types@7.4.4: - resolution: {integrity: sha512-B0VdgGb1XGEb9g3UuEd9xANCIhR3anvA3w0uYSG+7uMOflnEawwZksTSxvvoGM2hx9vC4pNT4Fci9sEC903UkA==} - dependencies: - '@storybook/channels': 7.4.4 - '@types/babel__core': 7.20.0 - '@types/express': 4.17.17 - file-system-cache: 2.3.0 - dev: true - - /@storybook/types@7.4.5: - resolution: {integrity: sha512-DTWFNjfRTpncjufDoUs0QnNkgHG2qThGKWL1D6sO18cYI02zWPyHWD8/cbqlvtT7XIGe3s1iUEfCTdU5GcwWBA==} - dependencies: - '@storybook/channels': 7.4.5 + '@storybook/channels': 7.4.6 '@types/babel__core': 7.20.0 '@types/express': 4.17.17 file-system-cache: 2.3.0 dev: true - /@storybook/vue3-vite@7.4.5(@vue/compiler-core@3.3.4)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(vite@4.4.9)(vue@3.3.4): - resolution: {integrity: sha512-hNuzSd7EAGpLNGekKjOfuMpir1CpMbSvro4q+04G34CGw2O6awoQKqE+gaOeAHIsSPffio5eeaBR1nKjoKYEog==} + /@storybook/vue3-vite@7.4.6(@vue/compiler-core@3.3.4)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(vite@4.4.11)(vue@3.3.4): + resolution: {integrity: sha512-r/mUDdCifpN99Cqmvm7IvPZGnur7lYiTxbQPhV8NdRBpQGxm3JC0life9yIvvHV9mYRCjn5MEzC65zWx03Nzig==} engines: {node: ^14.18 || >=16} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 vite: ^3.0.0 || ^4.0.0 dependencies: - '@storybook/builder-vite': 7.4.5(typescript@5.2.2)(vite@4.4.9) - '@storybook/core-server': 7.4.5 - '@storybook/vue3': 7.4.5(@vue/compiler-core@3.3.4)(vue@3.3.4) - '@vitejs/plugin-vue': 4.3.4(vite@4.4.9)(vue@3.3.4) + '@storybook/builder-vite': 7.4.6(typescript@5.2.2)(vite@4.4.11) + '@storybook/core-server': 7.4.6 + '@storybook/vue3': 7.4.6(@vue/compiler-core@3.3.4)(vue@3.3.4) + '@vitejs/plugin-vue': 4.4.0(vite@4.4.11)(vue@3.3.4) magic-string: 0.30.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.4)(sass@1.69.1)(terser@5.21.0) vue-docgen-api: 4.64.1(vue@3.3.4) transitivePeerDependencies: - '@preact/preset-vite' @@ -6882,30 +6896,30 @@ packages: - vue dev: true - /@storybook/vue3@7.4.5(@vue/compiler-core@3.3.4)(vue@3.3.4): - resolution: {integrity: sha512-9vmGSg+jwpTYeBneC3XAL5zJW7/kfA/3tXNfIOkqA4oJ087TBoo5XztzbtT6pSNq8fB9AY8VyPTG8ZE5IaJ4xQ==} + /@storybook/vue3@7.4.6(@vue/compiler-core@3.3.4)(vue@3.3.4): + resolution: {integrity: sha512-Azv/GhmPlAUy8UbXZHKubrBlKhGimuJTT2O6zUvIzggR6sJdsRmdWaEv2S90ZpMBkVYyyM9oKS1fZ4eKi/Ds8g==} engines: {node: '>=16.0.0'} peerDependencies: '@vue/compiler-core': ^3.0.0 vue: ^3.0.0 dependencies: - '@storybook/core-client': 7.4.5 - '@storybook/docs-tools': 7.4.5 + '@storybook/core-client': 7.4.6 + '@storybook/docs-tools': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/preview-api': 7.4.6 + '@storybook/types': 7.4.6 '@vue/compiler-core': 3.3.4 lodash: 4.17.21 ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.3.4 - vue-component-type-helpers: 1.8.15 + vue-component-type-helpers: 1.8.18 transitivePeerDependencies: - encoding - supports-color dev: true - /@swc/cli@0.1.62(@swc/core@1.3.90)(chokidar@3.5.3): + /@swc/cli@0.1.62(@swc/core@1.3.92)(chokidar@3.5.3): resolution: {integrity: sha512-kOFLjKY3XH1DWLfXL1/B5MizeNorHR8wHKEi92S/Zi9Md/AK17KSqR8MgyRJ6C1fhKHvbBCl8wboyKAFXStkYw==} engines: {node: '>= 12.13'} hasBin: true @@ -6917,7 +6931,7 @@ packages: optional: true dependencies: '@mole-inc/bin-wrapper': 8.0.1 - '@swc/core': 1.3.90 + '@swc/core': 1.3.92 chokidar: 3.5.3 commander: 7.2.0 fast-glob: 3.3.1 @@ -6946,8 +6960,8 @@ packages: dev: false optional: true - /@swc/core-darwin-arm64@1.3.90: - resolution: {integrity: sha512-he0w74HvcoufE6CZrB/U/VGVbc7021IQvYrn1geMACnq/OqMBqjdczNtdNfJAy87LZ4AOUjHDKEIjsZZu7o8nQ==} + /@swc/core-darwin-arm64@1.3.92: + resolution: {integrity: sha512-v7PqZUBtIF6Q5Cp48gqUiG8zQQnEICpnfNdoiY3xjQAglCGIQCjJIDjreZBoeZQZspB27lQN4eZ43CX18+2SnA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] @@ -6963,8 +6977,8 @@ packages: dev: false optional: true - /@swc/core-darwin-x64@1.3.90: - resolution: {integrity: sha512-hKNM0Ix0qMlAamPe0HUfaAhQVbZEL5uK6Iw8v9ew0FtVB4v7EifQ9n41wh+yCj0CjcHBPEBbQU0P6mNTxJu/RQ==} + /@swc/core-darwin-x64@1.3.92: + resolution: {integrity: sha512-Q3XIgQfXyxxxms3bPN+xGgvwk0TtG9l89IomApu+yTKzaIIlf051mS+lGngjnh9L0aUiCp6ICyjDLtutWP54fw==} engines: {node: '>=10'} cpu: [x64] os: [darwin] @@ -6991,8 +7005,8 @@ packages: dev: false optional: true - /@swc/core-linux-arm-gnueabihf@1.3.90: - resolution: {integrity: sha512-HumvtrqTWE8rlFuKt7If0ZL7145H/jVc4AeziVjcd+/ajpqub7IyfrLCYd5PmKMtfeSVDMsxjG0BJ0HLRxrTJA==} + /@swc/core-linux-arm-gnueabihf@1.3.92: + resolution: {integrity: sha512-tnOCoCpNVXC+0FCfG84PBZJyLlz0Vfj9MQhyhCvlJz9hQmvpf8nTdKH7RHrOn8VfxtUBLdVi80dXgIFgbvl7qA==} engines: {node: '>=10'} cpu: [arm] os: [linux] @@ -7008,8 +7022,8 @@ packages: dev: false optional: true - /@swc/core-linux-arm64-gnu@1.3.90: - resolution: {integrity: sha512-tA7DqCS7YCwngwXZQeqQhhMm8BbydpaABw8Z/EDQ7KPK1iZ1rNjZw+aWvSpmNmEGmH1RmQ9QDS9mGRDp0faAeg==} + /@swc/core-linux-arm64-gnu@1.3.92: + resolution: {integrity: sha512-lFfGhX32w8h1j74Iyz0Wv7JByXIwX11OE9UxG+oT7lG0RyXkF4zKyxP8EoxfLrDXse4Oop434p95e3UNC3IfCw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -7025,8 +7039,8 @@ packages: dev: false optional: true - /@swc/core-linux-arm64-musl@1.3.90: - resolution: {integrity: sha512-p2Vtid5BZA36fJkNUwk5HP+HJlKgTru+Ghna7pRe45ghKkkRIUk3fhkgudEvfKfhT+3AvP+GTVQ+T9k0gc9S8w==} + /@swc/core-linux-arm64-musl@1.3.92: + resolution: {integrity: sha512-rOZtRcLj57MSAbiecMsqjzBcZDuaCZ8F6l6JDwGkQ7u1NYR57cqF0QDyU7RKS1Jq27Z/Vg21z5cwqoH5fLN+Sg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -7042,8 +7056,8 @@ packages: dev: false optional: true - /@swc/core-linux-x64-gnu@1.3.90: - resolution: {integrity: sha512-J6pDtWaulYGXuANERuvv4CqmUbZOQrRZBCRQGZQJ6a86RWpesZqckBelnYx48wYmkgvMkF95Y3xbI3WTfoSHzw==} + /@swc/core-linux-x64-gnu@1.3.92: + resolution: {integrity: sha512-qptoMGnBL6v89x/Qpn+l1TH1Y0ed+v0qhNfAEVzZvCvzEMTFXphhlhYbDdpxbzRmCjH6GOGq7Y+xrWt9T1/ARg==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -7059,8 +7073,8 @@ packages: dev: false optional: true - /@swc/core-linux-x64-musl@1.3.90: - resolution: {integrity: sha512-3Gh6EA3+0K+l3MqnRON7h5bZ32xLmfcVM6QiHHJ9dBttq7YOEeEoMOCdIPMaQxJmK1VfLgZCsPYRd66MhvUSkw==} + /@swc/core-linux-x64-musl@1.3.92: + resolution: {integrity: sha512-g2KrJ43bZkCZHH4zsIV5ErojuV1OIpUHaEyW1gf7JWKaFBpWYVyubzFPvPkjcxHGLbMsEzO7w/NVfxtGMlFH/Q==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -7076,8 +7090,8 @@ packages: dev: false optional: true - /@swc/core-win32-arm64-msvc@1.3.90: - resolution: {integrity: sha512-BNaw/iJloDyaNOFV23Sr53ULlnbmzSoerTJ10v0TjSZOEIpsS0Rw6xOK1iI0voDJnRXeZeWRSxEC9DhefNtN/g==} + /@swc/core-win32-arm64-msvc@1.3.92: + resolution: {integrity: sha512-3MCRGPAYDoQ8Yyd3WsCMc8eFSyKXY5kQLyg/R5zEqA0uthomo0m0F5/fxAJMZGaSdYkU1DgF73ctOWOf+Z/EzQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] @@ -7093,8 +7107,8 @@ packages: dev: false optional: true - /@swc/core-win32-ia32-msvc@1.3.90: - resolution: {integrity: sha512-SiyTethWAheE/JbxXCukAAciU//PLcmVZ2ME92MRuLMLmOhrwksjbaa7ukj9WEF3LWrherhSqTXnpj3VC1l/qw==} + /@swc/core-win32-ia32-msvc@1.3.92: + resolution: {integrity: sha512-zqTBKQhgfWm73SVGS8FKhFYDovyRl1f5dTX1IwSKynO0qHkRCqJwauFJv/yevkpJWsI2pFh03xsRs9HncTQKSA==} engines: {node: '>=10'} cpu: [ia32] os: [win32] @@ -7110,16 +7124,16 @@ packages: dev: false optional: true - /@swc/core-win32-x64-msvc@1.3.90: - resolution: {integrity: sha512-OpWAW5ljKcPJ3SQ0pUuKqYfwXv7ssIhVgrH9XP9ONtdgXKWZRL9hqJQkcL55FARw/gDjKanoCM47wsTNQL+ZZA==} + /@swc/core-win32-x64-msvc@1.3.92: + resolution: {integrity: sha512-41bE66ddr9o/Fi1FBh0sHdaKdENPTuDpv1IFHxSg0dJyM/jX8LbkjnpdInYXHBxhcLVAPraVRrNsC4SaoPw2Pg==} engines: {node: '>=10'} cpu: [x64] os: [win32] requiresBuild: true optional: true - /@swc/core@1.3.90: - resolution: {integrity: sha512-wptBxP4PldOnhmyDVj8qUcn++GRqyw1qc9wOTGtPNHz8cpuTfdfIgYGlhI4La0UYqecuaaIfLfokyuNePOMHPg==} + /@swc/core@1.3.92: + resolution: {integrity: sha512-vx0vUrf4YTEw59njOJ46Ha5i0cZTMYdRHQ7KXU29efN1MxcmJH2RajWLPlvQarOP1ab9iv9cApD7SMchDyx2vA==} engines: {node: '>=10'} requiresBuild: true peerDependencies: @@ -7131,28 +7145,28 @@ packages: '@swc/counter': 0.1.1 '@swc/types': 0.1.5 optionalDependencies: - '@swc/core-darwin-arm64': 1.3.90 - '@swc/core-darwin-x64': 1.3.90 - '@swc/core-linux-arm-gnueabihf': 1.3.90 - '@swc/core-linux-arm64-gnu': 1.3.90 - '@swc/core-linux-arm64-musl': 1.3.90 - '@swc/core-linux-x64-gnu': 1.3.90 - '@swc/core-linux-x64-musl': 1.3.90 - '@swc/core-win32-arm64-msvc': 1.3.90 - '@swc/core-win32-ia32-msvc': 1.3.90 - '@swc/core-win32-x64-msvc': 1.3.90 + '@swc/core-darwin-arm64': 1.3.92 + '@swc/core-darwin-x64': 1.3.92 + '@swc/core-linux-arm-gnueabihf': 1.3.92 + '@swc/core-linux-arm64-gnu': 1.3.92 + '@swc/core-linux-arm64-musl': 1.3.92 + '@swc/core-linux-x64-gnu': 1.3.92 + '@swc/core-linux-x64-musl': 1.3.92 + '@swc/core-win32-arm64-msvc': 1.3.92 + '@swc/core-win32-ia32-msvc': 1.3.92 + '@swc/core-win32-x64-msvc': 1.3.92 /@swc/counter@0.1.1: resolution: {integrity: sha512-xVRaR4u9hcYjFvcSg71Lz5Bo4//CyjAAfMxa7UsaDSYxAshflUkVJWiyVWrfxC59z2kP1IzI4/1BEpnhI9o3Mw==} - /@swc/jest@0.2.29(@swc/core@1.3.90): + /@swc/jest@0.2.29(@swc/core@1.3.92): resolution: {integrity: sha512-8reh5RvHBsSikDC3WGCd5ZTd2BXKkyOdK7QwynrCH58jk2cQFhhHhFBg/jvnWZehUQe/EoOImLENc9/DwbBFow==} engines: {npm: '>= 7.0.0'} peerDependencies: '@swc/core': '*' dependencies: '@jest/create-cache-key-function': 27.5.1 - '@swc/core': 1.3.90 + '@swc/core': 1.3.92 jsonc-parser: 3.2.0 dev: true @@ -7329,7 +7343,7 @@ packages: pretty-format: 27.5.1 dev: true - /@testing-library/jest-dom@6.1.2(@types/jest@28.1.3)(vitest@0.34.5): + /@testing-library/jest-dom@6.1.2(@types/jest@28.1.3)(vitest@0.34.6): resolution: {integrity: sha512-NP9jl1Q2qDDtx+cqogowtQtmgD2OVs37iMSIsTv5eN5ETRkf26Kj6ugVwA93/gZzzFWQAsgkKkcftDe91BJCkQ==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} peerDependencies: @@ -7356,7 +7370,7 @@ packages: dom-accessibility-api: 0.5.16 lodash: 4.17.21 redent: 3.0.0 - vitest: 0.34.5(happy-dom@10.0.3)(sass@1.68.0)(terser@5.20.0) + vitest: 0.34.6(happy-dom@10.0.3)(sass@1.69.1)(terser@5.21.0) dev: true /@testing-library/user-event@14.4.3(@testing-library/dom@9.2.0): @@ -7404,7 +7418,7 @@ packages: /@types/accepts@1.3.5: resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/archiver@5.3.3: @@ -7458,7 +7472,7 @@ packages: resolution: {integrity: sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==} dependencies: '@types/connect': 3.4.35 - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/braces@3.0.1: @@ -7470,7 +7484,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.4 - '@types/node': 20.7.1 + '@types/node': 20.8.4 '@types/responselike': 1.0.0 dev: false @@ -7503,7 +7517,7 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/content-disposition@0.5.6: @@ -7517,7 +7531,7 @@ packages: /@types/cross-spawn@6.0.2: resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/debug@4.1.7: @@ -7571,7 +7585,7 @@ packages: /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 dev: true @@ -7592,20 +7606,20 @@ packages: /@types/fluent-ffmpeg@2.1.22: resolution: {integrity: sha512-ZZPDDrDOb2Ahp5fxZzuw64f0rCcviv+SDuCyJ1PIF/UFn9wNHtb/bY8Dj/2nrbQ7SNsGI7gaO2wJVkkU2HBcMg==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/glob@7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/graceful-fs@4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/hast@2.3.4: @@ -7620,7 +7634,7 @@ packages: /@types/http-link-header@1.0.3: resolution: {integrity: sha512-y8HkoD/vyid+5MrJ3aas0FvU3/BVBGcyG9kgxL0Zn4JwstA8CglFPnrR0RuzOjRCXwqzL5uxWC2IO7Ub0rMU2A==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/istanbul-lib-coverage@2.0.4: @@ -7664,7 +7678,7 @@ packages: /@types/jsdom@21.1.3: resolution: {integrity: sha512-1zzqSP+iHJYV4lB3lZhNBa012pubABkj9yG/GuXuf6LZH1cSPIJBqFDrm5JX65HHt6VOnNYdTui/0ySerRbMgA==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 '@types/tough-cookie': 4.0.2 parse5: 7.1.2 dev: true @@ -7688,7 +7702,7 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: false /@types/lodash@4.14.191: @@ -7737,7 +7751,7 @@ packages: /@types/node-fetch@2.6.4: resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 form-data: 3.0.1 /@types/node-fetch@3.0.3: @@ -7754,13 +7768,15 @@ packages: resolution: {integrity: sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==} dev: true - /@types/node@20.7.1: - resolution: {integrity: sha512-LT+OIXpp2kj4E2S/p91BMe+VgGX2+lfO+XTpfXhh+bCk2LkQtHZSub8ewFBMGP5ClysPjTDFa4sMI8Q3n4T0wg==} + /@types/node@20.8.4: + resolution: {integrity: sha512-ZVPnqU58giiCjSxjVUESDtdPk4QR5WQhhINbc9UBrKLU68MX5BF6kbQzTrkwbolyr0X8ChBpXfavr5mZFKZQ5A==} + dependencies: + undici-types: 5.25.3 /@types/nodemailer@6.4.11: resolution: {integrity: sha512-Ld2c0frwpGT4VseuoeboCXQ7UJIkK3X7Lx/4YsZEiUHtHsthWAOCYtf6PAiLhMtfwV0cWJRabLBS3+LD8x6Nrw==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/normalize-package-data@2.4.1: @@ -7777,13 +7793,13 @@ packages: resolution: {integrity: sha512-U3L0c4eQA6lTSZRgW4LYfhKlR084Aw19akmYHrMdYzaqg9mQDfc2b/1iyqm9+1FJDEnVS5ONi5fxdDrB4/7CpQ==} dependencies: '@types/express': 4.17.17 - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/oauth@0.9.2: resolution: {integrity: sha512-Nu3/abQ6yR9VlsCdX3aiGsWFkj6OJvJqDvg/36t8Gwf2mFXdBZXPDN3K+2yfeA6Lo2m1Q12F8Qil9TZ48nWhOQ==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/offscreencanvas@2019.3.0: @@ -7796,10 +7812,10 @@ packages: requiresBuild: true dev: false - /@types/pg@8.10.3: - resolution: {integrity: sha512-BACzsw64lCZesclRpZGu55tnqgFAYcrCBP92xLh1KLypZLCOsvJTSTgaoFVTy3lCys/aZTQzfeDxtjwrvdzL2g==} + /@types/pg@8.10.4: + resolution: {integrity: sha512-6cxJPHzhlJxqAMkWl2w3KubTEM0UjGC0UrtIToa9J/CEuRFJ2bquKt+g9MhYBN9n1+U6UZZ8CW6Z4oLx/Tvh/w==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 pg-protocol: 1.6.0 pg-types: 4.0.1 dev: true @@ -7823,7 +7839,7 @@ packages: /@types/qrcode@1.5.2: resolution: {integrity: sha512-W4KDz75m7rJjFbyCctzCtRzZUj+PrUHV+YjqDp50sSRezTbrtEAIq2iTzC6lISARl3qw+8IlcCyljdcVJE0Wug==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/qs@6.9.7: @@ -7853,7 +7869,7 @@ packages: /@types/readdir-glob@1.1.1: resolution: {integrity: sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/rename@1.0.5: @@ -7863,7 +7879,7 @@ packages: /@types/responselike@1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: false /@types/sanitize-html@2.9.1: @@ -7889,7 +7905,7 @@ packages: resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} dependencies: '@types/mime': 3.0.1 - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/serviceworker@0.0.67: @@ -7899,7 +7915,7 @@ packages: /@types/set-cookie-parser@2.4.3: resolution: {integrity: sha512-7QhnH7bi+6KAhBB+Auejz1uV9DHiopZqu7LfR/5gZZTkejJV5nYeZZpgfFoE0N8aDsXuiYpfKyfyMatCwQhyTQ==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/sharp@0.32.0: @@ -7955,20 +7971,20 @@ packages: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} dev: true - /@types/uuid@9.0.4: - resolution: {integrity: sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==} + /@types/uuid@9.0.5: + resolution: {integrity: sha512-xfHdwa1FMJ082prjSJpoEI57GZITiQz10r3vEJCHa2khEFQjKy91aWKz6+zybzssCvXUwE1LQWgWVwZ4nYUvHQ==} dev: true /@types/vary@1.1.1: resolution: {integrity: sha512-XL8U62BpXBMMuFzFBYsWekQwo+dqcyN117IwFVMCkBCvc6HY1ODdRKNA0JHxnuTM5lX3kpqsnBH5OuEeXSN3aA==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/web-push@3.6.1: resolution: {integrity: sha512-Zu6Iju7c4IlE8I8eEeFLYRb7XFqvHFmWWAYr1cmug9EX3c6CDarxIXWN/GO0sxjbJLkHPwozUzp6cLdXsrq7Ew==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/webgl-ext@0.0.30: @@ -7979,13 +7995,13 @@ packages: /@types/websocket@1.0.7: resolution: {integrity: sha512-62Omr8U0PO+hgjLCpPnMsmjh2/FRwIGOktZHyYAUzooEJotwkXHMp7vCacdYi8haxBNOiw9bc2HIHI+b/MPNjA==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/ws@8.5.6: resolution: {integrity: sha512-8B5EO9jLVCy+B58PLHvLDuOD8DRVMgQzq8d55SjLCOn9kqGyqOvy27exVaTio1q1nX5zLu8/6N0n2ThSxOM6tg==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /@types/yargs-parser@21.0.0: @@ -8008,12 +8024,12 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true optional: true - /@typescript-eslint/eslint-plugin@6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.50.0)(typescript@5.2.2): - resolution: {integrity: sha512-vntq452UHNltxsaaN+L9WyuMch8bMd9CqJ3zhzTPXXidwbf5mqqKCVXEuvRZUqLJSTLeWE65lQwyXsRGnXkCTA==} + /@typescript-eslint/eslint-plugin@6.7.5(@typescript-eslint/parser@6.7.5)(eslint@8.51.0)(typescript@5.2.2): + resolution: {integrity: sha512-JhtAwTRhOUcP96D0Y6KYnwig/MRQbOoLGXTON2+LlyB/N35SP9j1boai2zzwXb7ypKELXMx3DVk9UTaEq1vHEw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha @@ -8024,13 +8040,13 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.6.2 - '@typescript-eslint/parser': 6.7.3(eslint@8.50.0)(typescript@5.2.2) - '@typescript-eslint/scope-manager': 6.7.3 - '@typescript-eslint/type-utils': 6.7.3(eslint@8.50.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.7.3(eslint@8.50.0)(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.7.3 + '@typescript-eslint/parser': 6.7.5(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.7.5 + '@typescript-eslint/type-utils': 6.7.5(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.7.5(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.7.5 debug: 4.3.4(supports-color@8.1.1) - eslint: 8.50.0 + eslint: 8.51.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare: 1.4.0 @@ -8041,8 +8057,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.7.3(eslint@8.50.0)(typescript@5.2.2): - resolution: {integrity: sha512-TlutE+iep2o7R8Lf+yoer3zU6/0EAUc8QIBB3GYBc1KGz4c4TRm83xwXUZVPlZ6YCLss4r77jbu6j3sendJoiQ==} + /@typescript-eslint/parser@6.7.5(eslint@8.51.0)(typescript@5.2.2): + resolution: {integrity: sha512-bIZVSGx2UME/lmhLcjdVc7ePBwn7CLqKarUBL4me1C5feOd663liTGjMBGVcGr+BhnSLeP4SgwdvNnnkbIdkCw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -8051,27 +8067,27 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.7.3 - '@typescript-eslint/types': 6.7.3 - '@typescript-eslint/typescript-estree': 6.7.3(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.7.3 + '@typescript-eslint/scope-manager': 6.7.5 + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/typescript-estree': 6.7.5(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.7.5 debug: 4.3.4(supports-color@8.1.1) - eslint: 8.50.0 + eslint: 8.51.0 typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/scope-manager@6.7.3: - resolution: {integrity: sha512-wOlo0QnEou9cHO2TdkJmzF7DFGvAKEnB82PuPNHpT8ZKKaZu6Bm63ugOTn9fXNJtvuDPanBc78lGUGGytJoVzQ==} + /@typescript-eslint/scope-manager@6.7.5: + resolution: {integrity: sha512-GAlk3eQIwWOJeb9F7MKQ6Jbah/vx1zETSDw8likab/eFcqkjSD7BI75SDAeC5N2L0MmConMoPvTsmkrg71+B1A==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.7.3 - '@typescript-eslint/visitor-keys': 6.7.3 + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/visitor-keys': 6.7.5 dev: true - /@typescript-eslint/type-utils@6.7.3(eslint@8.50.0)(typescript@5.2.2): - resolution: {integrity: sha512-Fc68K0aTDrKIBvLnKTZ5Pf3MXK495YErrbHb1R6aTpfK5OdSFj0rVN7ib6Tx6ePrZ2gsjLqr0s98NG7l96KSQw==} + /@typescript-eslint/type-utils@6.7.5(eslint@8.51.0)(typescript@5.2.2): + resolution: {integrity: sha512-Gs0qos5wqxnQrvpYv+pf3XfcRXW6jiAn9zE/K+DlmYf6FcpxeNYN0AIETaPR7rHO4K2UY+D0CIbDP9Ut0U4m1g==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -8080,23 +8096,23 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.7.3(typescript@5.2.2) - '@typescript-eslint/utils': 6.7.3(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.7.5(typescript@5.2.2) + '@typescript-eslint/utils': 6.7.5(eslint@8.51.0)(typescript@5.2.2) debug: 4.3.4(supports-color@8.1.1) - eslint: 8.50.0 + eslint: 8.51.0 ts-api-utils: 1.0.1(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/types@6.7.3: - resolution: {integrity: sha512-4g+de6roB2NFcfkZb439tigpAMnvEIg3rIjWQ+EM7IBaYt/CdJt6em9BJ4h4UpdgaBWdmx2iWsafHTrqmgIPNw==} + /@typescript-eslint/types@6.7.5: + resolution: {integrity: sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@6.7.3(typescript@5.2.2): - resolution: {integrity: sha512-YLQ3tJoS4VxLFYHTw21oe1/vIZPRqAO91z6Uv0Ss2BKm/Ag7/RVQBcXTGcXhgJMdA4U+HrKuY5gWlJlvoaKZ5g==} + /@typescript-eslint/typescript-estree@6.7.5(typescript@5.2.2): + resolution: {integrity: sha512-NhJiJ4KdtwBIxrKl0BqG1Ur+uw7FiOnOThcYx9DpOGJ/Abc9z2xNzLeirCG02Ig3vkvrc2qFLmYSSsaITbKjlg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -8104,8 +8120,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.7.3 - '@typescript-eslint/visitor-keys': 6.7.3 + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/visitor-keys': 6.7.5 debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 @@ -8116,34 +8132,34 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.7.3(eslint@8.50.0)(typescript@5.2.2): - resolution: {integrity: sha512-vzLkVder21GpWRrmSR9JxGZ5+ibIUSudXlW52qeKpzUEQhRSmyZiVDDj3crAth7+5tmN1ulvgKaCU2f/bPRCzg==} + /@typescript-eslint/utils@6.7.5(eslint@8.51.0)(typescript@5.2.2): + resolution: {integrity: sha512-pfRRrH20thJbzPPlPc4j0UNGvH1PjPlhlCMq4Yx7EGjV7lvEeGX0U6MJYe8+SyFutWgSHsdbJ3BXzZccYggezA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.51.0) '@types/json-schema': 7.0.12 '@types/semver': 7.5.3 - '@typescript-eslint/scope-manager': 6.7.3 - '@typescript-eslint/types': 6.7.3 - '@typescript-eslint/typescript-estree': 6.7.3(typescript@5.2.2) - eslint: 8.50.0 + '@typescript-eslint/scope-manager': 6.7.5 + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/typescript-estree': 6.7.5(typescript@5.2.2) + eslint: 8.51.0 semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/visitor-keys@6.7.3: - resolution: {integrity: sha512-HEVXkU9IB+nk9o63CeICMHxFWbHWr3E1mpilIQBe9+7L/lH97rleFLVtYsfnWB+JVMaiFnEaxvknvmIzX+CqVg==} + /@typescript-eslint/visitor-keys@6.7.5: + resolution: {integrity: sha512-3MaWdDZtLlsexZzDSdQWsFQ9l9nL8B80Z4fImSpyllFC/KLqWQRdEcB+gGGO+N3Q2uL40EsG66wZLsohPxNXvg==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.7.3 + '@typescript-eslint/types': 6.7.5 eslint-visitor-keys: 3.4.3 dev: true - /@vitejs/plugin-react@3.1.0(vite@4.4.9): + /@vitejs/plugin-react@3.1.0(vite@4.4.11): resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -8154,23 +8170,23 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.22.11) magic-string: 0.27.0 react-refresh: 0.14.0 - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.4)(sass@1.69.1)(terser@5.21.0) transitivePeerDependencies: - supports-color dev: true - /@vitejs/plugin-vue@4.3.4(vite@4.4.9)(vue@3.3.4): - resolution: {integrity: sha512-ciXNIHKPriERBisHFBvnTbfKa6r9SAesOYXeGDzgegcvy9Q4xdScSHAmKbNT0M3O0S9LKhIf5/G+UYG4NnnzYw==} + /@vitejs/plugin-vue@4.4.0(vite@4.4.11)(vue@3.3.4): + resolution: {integrity: sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.0.0 vue: ^3.2.25 dependencies: - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.4)(sass@1.69.1)(terser@5.21.0) vue: 3.3.4 - /@vitest/coverage-v8@0.34.5(vitest@0.34.5): - resolution: {integrity: sha512-97xjhRTSdmeeHCm2nNHhT3hLsMYkAhHXm/rwj6SZ3voka8xiCJrwgtfIjoZIFEL4OO0KezGmVuHWQXcMunULIA==} + /@vitest/coverage-v8@0.34.6(vitest@0.34.6): + resolution: {integrity: sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==} peerDependencies: vitest: '>=0.32.0 <1' dependencies: @@ -8185,68 +8201,68 @@ packages: std-env: 3.3.3 test-exclude: 6.0.0 v8-to-istanbul: 9.1.0 - vitest: 0.34.5(happy-dom@10.0.3)(sass@1.68.0)(terser@5.20.0) + vitest: 0.34.6(happy-dom@10.0.3)(sass@1.69.1)(terser@5.21.0) transitivePeerDependencies: - supports-color dev: true - /@vitest/expect@0.34.5: - resolution: {integrity: sha512-/3RBIV9XEH+nRpRMqDJBufKIOQaYUH2X6bt0rKSCW0MfKhXFLYsR5ivHifeajRSTsln0FwJbitxLKHSQz/Xwkw==} + /@vitest/expect@0.34.6: + resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} dependencies: - '@vitest/spy': 0.34.5 - '@vitest/utils': 0.34.5 - chai: 4.3.7 + '@vitest/spy': 0.34.6 + '@vitest/utils': 0.34.6 + chai: 4.3.10 dev: true - /@vitest/runner@0.34.5: - resolution: {integrity: sha512-RDEE3ViVvl7jFSCbnBRyYuu23XxmvRTSZWW6W4M7eC5dOsK75d5LIf6uhE5Fqf809DQ1+9ICZZNxhIolWHU4og==} + /@vitest/runner@0.34.6: + resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==} dependencies: - '@vitest/utils': 0.34.5 + '@vitest/utils': 0.34.6 p-limit: 4.0.0 pathe: 1.1.1 dev: true - /@vitest/snapshot@0.34.5: - resolution: {integrity: sha512-+ikwSbhu6z2yOdtKmk/aeoDZ9QPm2g/ZO5rXT58RR9Vmu/kB2MamyDSx77dctqdZfP3Diqv4mbc/yw2kPT8rmA==} + /@vitest/snapshot@0.34.6: + resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==} dependencies: magic-string: 0.30.3 pathe: 1.1.1 pretty-format: 29.7.0 dev: true - /@vitest/spy@0.34.5: - resolution: {integrity: sha512-epsicsfhvBjRjCMOC/3k00mP/TBGQy8/P0DxOFiWyLt55gnZ99dqCfCiAsKO17BWVjn4eZRIjKvcqNmSz8gvmg==} + /@vitest/spy@0.34.6: + resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==} dependencies: tinyspy: 2.1.1 dev: true - /@vitest/utils@0.34.5: - resolution: {integrity: sha512-ur6CmmYQoeHMwmGb0v+qwkwN3yopZuZyf4xt1DBBSGBed8Hf9Gmbm/5dEWqgpLPdRx6Av6jcWXrjcKfkTzg/pw==} + /@vitest/utils@0.34.6: + resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} dependencies: diff-sequences: 29.6.3 loupe: 2.3.6 pretty-format: 29.7.0 dev: true - /@volar/language-core@1.10.0: - resolution: {integrity: sha512-ddyWwSYqcbEZNFHm+Z3NZd6M7Ihjcwl/9B5cZd8kECdimVXUFdFi60XHWD27nrWtUQIsUYIG7Ca1WBwV2u2LSQ==} + /@volar/language-core@1.10.3: + resolution: {integrity: sha512-7Qgwu9bWUHN+cLrOkCbIVBkL+RVPREhvY07wY89dGxi4mY9mQCsUVRRp64F61lX7Nc27meMnvy0sWlzY0x6oQQ==} dependencies: - '@volar/source-map': 1.10.0 + '@volar/source-map': 1.10.3 dev: true - /@volar/source-map@1.10.0: - resolution: {integrity: sha512-/ibWdcOzDGiq/GM1JU2eX8fH1bvAhl66hfe8yEgLEzg9txgr6qb5sQ/DEz5PcDL75tF5H5sCRRwn8Eu8ezi9mw==} + /@volar/source-map@1.10.3: + resolution: {integrity: sha512-QE9nwK3xsdBQGongHnC9SCR0itx7xUKQFsUDn5HbZY3pHpyXxdY1hSBG0eh9mE+aTKoM4KlqMvrb+19Tv9vS1Q==} dependencies: muggle-string: 0.3.1 dev: true - /@volar/typescript@1.10.0: - resolution: {integrity: sha512-OtqGtFbUKYC0pLNIk3mHQp5xWnvL1CJIUc9VE39VdZ/oqpoBh5jKfb9uJ45Y4/oP/WYTrif/Uxl1k8VTPz66Gg==} + /@volar/typescript@1.10.3: + resolution: {integrity: sha512-n0ar6xGYpRoSvgGMetm/JXP0QAXx+NOUvxCaWCfCjiFivQRSLJeydYDijhoGBUl5KSKosqq9In5L3e/m2TqTcQ==} dependencies: - '@volar/language-core': 1.10.0 + '@volar/language-core': 1.10.3 dev: true - /@vue-macros/common@1.8.0(rollup@3.29.4)(vue@3.3.4): + /@vue-macros/common@1.8.0(rollup@4.0.2)(vue@3.3.4): resolution: {integrity: sha512-auDJJzE0z3uRe3867e0DsqcseKImktNf5ojCZgUKqiVxb2yTlwlgOVAYCgoep9oITqxkXQymSvFeKhedi8PhaA==} engines: {node: '>=16.14.0'} peerDependencies: @@ -8256,9 +8272,9 @@ packages: optional: true dependencies: '@babel/types': 7.22.17 - '@rollup/pluginutils': 5.0.4(rollup@3.29.4) + '@rollup/pluginutils': 5.0.5(rollup@4.0.2) '@vue/compiler-sfc': 3.3.4 - ast-kit: 0.11.2(rollup@3.29.4) + ast-kit: 0.11.2(rollup@4.0.2) local-pkg: 0.4.3 magic-string-ast: 0.3.0 vue: 3.3.4 @@ -8266,14 +8282,14 @@ packages: - rollup dev: false - /@vue-macros/reactivity-transform@0.3.23(rollup@3.29.4)(vue@3.3.4): + /@vue-macros/reactivity-transform@0.3.23(rollup@4.0.2)(vue@3.3.4): resolution: {integrity: sha512-SubIg1GsNpQdIDJusrcA2FWBgwSY+4jmL0j6SJ6PU85r3rlS+uDhn6AUkqxeZRAdmJnrbGHXDyWUdygOZmWrSg==} engines: {node: '>=16.14.0'} peerDependencies: vue: ^2.7.0 || ^3.2.25 dependencies: '@babel/parser': 7.22.16 - '@vue-macros/common': 1.8.0(rollup@3.29.4)(vue@3.3.4) + '@vue-macros/common': 1.8.0(rollup@4.0.2)(vue@3.3.4) '@vue/compiler-core': 3.3.4 '@vue/shared': 3.3.4 magic-string: 0.30.3 @@ -8317,20 +8333,20 @@ packages: '@vue/compiler-dom': 3.3.4 '@vue/shared': 3.3.4 - /@vue/language-core@1.8.15(typescript@5.2.2): - resolution: {integrity: sha512-zche5Aw8kkvp3YaghuLiOZyVIpoWHjSQ0EfjxGSsqHOPMamdCoa9x3HtbenpR38UMUoKJ88wiWuiOrV3B/Yq+A==} + /@vue/language-core@1.8.18(typescript@5.2.2): + resolution: {integrity: sha512-byTi+mwSL7XnVRtfWE3MJy3HQryoVSQ3lymauXviegn3G1wwwlSOUljzQe3w5PyesOnBEIxYoavfKzMJnExrBA==} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@volar/language-core': 1.10.0 - '@volar/source-map': 1.10.0 + '@volar/language-core': 1.10.3 + '@volar/source-map': 1.10.3 '@vue/compiler-dom': 3.3.4 '@vue/reactivity': 3.3.4 '@vue/shared': 3.3.4 - minimatch: 9.0.2 + minimatch: 9.0.3 muggle-string: 0.3.1 typescript: 5.2.2 vue-template-compiler: 2.7.14 @@ -8387,11 +8403,11 @@ packages: '@vue/server-renderer': 3.3.4(vue@3.3.4) dev: true - /@vue/typescript@1.8.15(typescript@5.2.2): - resolution: {integrity: sha512-qWyanQKXOsK84S8rP7QBrqsvUdQ0nZABZmTjXMpb3ox4Bp5IbkscREA3OPUrkgl64mAxwwCzIWcOc3BPTCPjQw==} + /@vue/typescript@1.8.18(typescript@5.2.2): + resolution: {integrity: sha512-3M+lu+DUwJW0fNwd/rLE0FenmELxcC6zxgm/YZ25jSTi+uNGj9L5XvXvf20guC69gQvZ+cg49tTxbepfFVuNNQ==} dependencies: - '@volar/typescript': 1.10.0 - '@vue/language-core': 1.8.15(typescript@5.2.2) + '@volar/typescript': 1.10.3 + '@vue/language-core': 1.8.18(typescript@5.2.2) transitivePeerDependencies: - typescript dev: true @@ -8848,12 +8864,12 @@ packages: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} dev: true - /ast-kit@0.11.2(rollup@3.29.4): + /ast-kit@0.11.2(rollup@4.0.2): resolution: {integrity: sha512-Q0DjXK4ApbVoIf9GLyCo252tUH44iTnD/hiJ2TQaJeydYWSpKk0sI34+WMel8S9Wt5pbLgG02oJ+gkgX5DV3sQ==} engines: {node: '>=16.14.0'} dependencies: '@babel/parser': 7.22.16 - '@rollup/pluginutils': 5.0.4(rollup@3.29.4) + '@rollup/pluginutils': 5.0.5(rollup@4.0.2) pathe: 1.1.1 transitivePeerDependencies: - rollup @@ -9317,8 +9333,8 @@ packages: dependencies: node-gyp-build: 4.6.0 - /bullmq@4.11.4: - resolution: {integrity: sha512-LuCR3ILngYa3CLC5jyf8DU4Yokj9T12MWwBogP3S4IiJUtbJsQ9GTGFxho3imRxXfcd9DUfrABT/pSoqVigXiQ==} + /bullmq@4.12.3: + resolution: {integrity: sha512-4uPp4NQTALFF+eFK7g8VJM+rt0aiduQdzBomgiEO1OK4OE+TdgC6cjGXooKI/asuB8iDhSZ+pSnGYy5Xyr6qRA==} dependencies: cron-parser: 4.8.1 glob: 8.1.0 @@ -9514,14 +9530,14 @@ packages: dependencies: nofilter: 3.1.0 - /chai@4.3.7: - resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + /chai@4.3.10: + resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} engines: {node: '>=4'} dependencies: assertion-error: 1.1.0 - check-error: 1.0.2 + check-error: 1.0.3 deep-eql: 4.1.3 - get-func-name: 2.0.0 + get-func-name: 2.0.2 loupe: 2.3.6 pathval: 1.1.1 type-detect: 4.0.8 @@ -9630,8 +9646,10 @@ packages: hammerjs: 2.0.8 dev: false - /check-error@1.0.2: - resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 dev: true /check-more-types@2.24.0: @@ -9683,8 +9701,8 @@ packages: engines: {node: '>=10'} requiresBuild: true - /chromatic@7.2.0: - resolution: {integrity: sha512-EbuvmsM6XAVFC4EQpqR2AT2PaXY4IS8qWxxg6N10AhpRulfX2b2AtW1hUc88cCosRyztd6esxkBdj3FSKR7zVw==} + /chromatic@7.2.3: + resolution: {integrity: sha512-UEcHB1nkPoHWoRybPzv6BOVqPr7PqDNuz3u8NCRg7KJciouOb20HjiUQx4Dh9mgA7JUsb2WeGHE2SG/0fHH0PA==} hasBin: true dev: false @@ -10021,7 +10039,7 @@ packages: readable-stream: 3.6.0 dev: false - /create-jest@29.7.0(@types/node@20.7.1): + /create-jest@29.7.0(@types/node@20.8.4): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10030,7 +10048,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.7.1) + jest-config: 29.7.0(@types/node@20.8.4) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -11045,7 +11063,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.3)(eslint-import-resolver-node@0.3.7)(eslint@8.50.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.7)(eslint@8.51.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -11066,15 +11084,15 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.7.3(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.5(eslint@8.51.0)(typescript@5.2.2) debug: 3.2.7(supports-color@5.5.0) - eslint: 8.50.0 + eslint: 8.51.0 eslint-import-resolver-node: 0.3.7 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.7.3)(eslint@8.50.0): + /eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.7.5)(eslint@8.51.0): resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==} engines: {node: '>=4'} peerDependencies: @@ -11084,16 +11102,16 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.7.3(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.5(eslint@8.51.0)(typescript@5.2.2) array-includes: 3.1.6 array.prototype.findlastindex: 1.2.2 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 - eslint: 8.50.0 + eslint: 8.51.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.3)(eslint-import-resolver-node@0.3.7)(eslint@8.50.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.7)(eslint@8.51.0) has: 1.0.3 is-core-module: 2.13.0 is-glob: 4.0.3 @@ -11109,19 +11127,19 @@ packages: - supports-color dev: true - /eslint-plugin-vue@9.17.0(eslint@8.50.0): + /eslint-plugin-vue@9.17.0(eslint@8.51.0): resolution: {integrity: sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0) - eslint: 8.50.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.51.0) + eslint: 8.51.0 natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.0.13 semver: 7.5.4 - vue-eslint-parser: 9.3.1(eslint@8.50.0) + vue-eslint-parser: 9.3.2(eslint@8.51.0) xml-name-validator: 4.0.0 transitivePeerDependencies: - supports-color @@ -11131,14 +11149,6 @@ packages: resolution: {integrity: sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==} dev: true - /eslint-scope@7.2.0: - resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - dev: true - /eslint-scope@7.2.2: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -11147,25 +11157,20 @@ packages: estraverse: 5.3.0 dev: true - /eslint-visitor-keys@3.4.1: - resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - /eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.50.0: - resolution: {integrity: sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==} + /eslint@8.51.0: + resolution: {integrity: sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.51.0) '@eslint-community/regexpp': 4.6.2 '@eslint/eslintrc': 2.1.2 - '@eslint/js': 8.50.0 + '@eslint/js': 8.51.0 '@humanwhocodes/config-array': 0.11.11 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 @@ -11203,15 +11208,6 @@ packages: - supports-color dev: true - /espree@9.5.2: - resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - acorn: 8.10.0 - acorn-jsx: 5.3.2(acorn@8.10.0) - eslint-visitor-keys: 3.4.3 - dev: true - /espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -12000,8 +11996,8 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - /get-func-name@2.0.0: - resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true /get-intrinsic@1.2.0: @@ -12287,8 +12283,8 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true - /graphql@16.6.0: - resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==} + /graphql@16.8.1: + resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} dev: true @@ -13250,7 +13246,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.4 chalk: 4.1.2 co: 4.6.0 dedent: 1.3.0 @@ -13271,7 +13267,7 @@ packages: - supports-color dev: true - /jest-cli@29.7.0(@types/node@20.7.1): + /jest-cli@29.7.0(@types/node@20.8.4): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -13285,10 +13281,10 @@ packages: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.7.1) + create-jest: 29.7.0(@types/node@20.8.4) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.7.1) + jest-config: 29.7.0(@types/node@20.8.4) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.6.2 @@ -13299,7 +13295,7 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@20.7.1): + /jest-config@29.7.0(@types/node@20.8.4): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -13314,7 +13310,7 @@ packages: '@babel/core': 7.22.11 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.4 babel-jest: 29.7.0(@babel/core@7.22.11) chalk: 4.1.2 ci-info: 3.7.1 @@ -13394,7 +13390,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.4 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -13424,7 +13420,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.6 - '@types/node': 20.7.1 + '@types/node': 20.8.4 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -13485,7 +13481,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 20.7.1 + '@types/node': 20.8.4 dev: true /jest-mock@29.7.0: @@ -13493,7 +13489,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.4 jest-util: 29.7.0 dev: true @@ -13548,7 +13544,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.4 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -13579,7 +13575,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.4 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 @@ -13631,7 +13627,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.4 chalk: 4.1.2 ci-info: 3.7.1 graceful-fs: 4.2.11 @@ -13656,7 +13652,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.4 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -13675,13 +13671,13 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest@29.7.0(@types/node@20.7.1): + /jest@29.7.0(@types/node@20.8.4): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -13694,7 +13690,7 @@ packages: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.7.1) + jest-cli: 29.7.0(@types/node@20.8.4) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -13957,8 +13953,8 @@ packages: resolution: {integrity: sha512-bQmbVtsfbgaKBTWCKiDCPlUPbdlRIK/FzSwT3BzIgZl/cU6TqXu6pZJsCI/dJVrZ9Gir5GC4woqw9shH/v7MBw==} dev: false - /jssha@3.3.0: - resolution: {integrity: sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w==} + /jssha@3.3.1: + resolution: {integrity: sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==} dev: false /jstransformer@1.0.0: @@ -14188,7 +14184,7 @@ packages: /loupe@2.3.6: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} dependencies: - get-func-name: 2.0.0 + get-func-name: 2.0.2 dev: true /lowercase-keys@2.0.0: @@ -14521,6 +14517,13 @@ packages: dependencies: brace-expansion: 2.0.1 + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} @@ -14664,10 +14667,6 @@ packages: /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - /ms@2.1.1: - resolution: {integrity: sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==} - dev: true - /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -14701,17 +14700,17 @@ packages: msgpackr-extract: 3.0.2 dev: false - /msw-storybook-addon@1.8.0(msw@1.3.1): + /msw-storybook-addon@1.8.0(msw@1.3.2): resolution: {integrity: sha512-dw3vZwqjixmiur0vouRSOax7wPSu9Og2Hspy9JZFHf49bZRjwDiLF0Pfn2NXEkGviYJOJiGxS1ejoTiUwoSg4A==} peerDependencies: msw: '>=0.35.0 <2.0.0' dependencies: is-node-process: 1.0.1 - msw: 1.3.1(typescript@5.2.2) + msw: 1.3.2(typescript@5.2.2) dev: true - /msw@1.3.1(typescript@5.2.2): - resolution: {integrity: sha512-GhP5lHSTXNlZb9EaKgPRJ01YAnVXwzkvnTzRn4W8fxU2DXuJrRO+Nb6OHdYqB4fCkwSNpIJH9JkON5Y6rHqJMQ==} + /msw@1.3.2(typescript@5.2.2): + resolution: {integrity: sha512-wKLhFPR+NitYTkQl5047pia0reNGgf0P6a1eTnA5aNlripmiz0sabMvvHcicE8kQ3/gZcI0YiPFWmYfowfm3lA==} engines: {node: '>=14'} hasBin: true requiresBuild: true @@ -14729,7 +14728,7 @@ packages: chalk: 4.1.2 chokidar: 3.5.3 cookie: 0.4.2 - graphql: 16.6.0 + graphql: 16.8.1 headers-polyfill: 3.2.5 inquirer: 8.2.5 is-node-process: 1.2.0 @@ -14983,8 +14982,8 @@ packages: /node-releases@2.0.13: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} - /nodemailer@6.9.5: - resolution: {integrity: sha512-/dmdWo62XjumuLc5+AYQZeiRj+PRR8y8qKtFCOyuOl1k/hckZd8durUUHs/ucKx6/8kN+wFxqKJlQ/LK/qR5FA==} + /nodemailer@6.9.6: + resolution: {integrity: sha512-s7pDtWwe5fLMkQUhw8TkWB/wnZ7SRdd9HRZslq/s24hlZvBP3j32N/ETLmnqTpmj4xoBZL9fOWyCIZ7r2HORHg==} engines: {node: '>=6.0.0'} dev: false @@ -15320,10 +15319,10 @@ packages: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} dev: true - /otpauth@9.1.4: - resolution: {integrity: sha512-T6T0E1WlzwKWESq8K0Ja47u01XjmDmRY/AiUoMAc6xZI/OsTsD4cqBrfpt2WfJ29W5pRiWkuUuyHdNQl0/Ic+Q==} + /otpauth@9.1.5: + resolution: {integrity: sha512-mnic91MZxvj04Ir7FN8Xi6wF3FU8D+s6M5p6FQaSS91/csKswoOI9Dk7kKSnGFAoBYgGTTO+OWScV0nJuzrbPg==} dependencies: - jssha: 3.3.0 + jssha: 3.3.1 dev: false /outvariant@1.4.0: @@ -15653,8 +15652,8 @@ packages: split2: 4.1.0 dev: false - /photoswipe@5.4.1: - resolution: {integrity: sha512-iauO0fP4oMdZvjlXzeIe8um1fZatkGE0bqdoIwpb65jlo/KK1KhfD7Z51+0YhS2tC4FOoOtE1p0c4o/HbY1s2Q==} + /photoswipe@5.4.2: + resolution: {integrity: sha512-z5hr36nAIPOZbHJPbCJ/mQ3+ZlizttF9za5gKXKH/us1k4KNHaRbC63K1Px5sVVKUtGb/2+ixHpKqtwl0WAwvA==} engines: {node: '>= 0.12.0'} dev: false @@ -17160,6 +17159,25 @@ packages: optionalDependencies: fsevents: 2.3.2 + /rollup@4.0.2: + resolution: {integrity: sha512-MCScu4usMPCeVFaiLcgMDaBQeYi1z6vpWxz0r0hq0Hv77Y2YuOTZldkuNJ54BdYBH3e+nkrk6j0Rre/NLDBYzg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.0.2 + '@rollup/rollup-android-arm64': 4.0.2 + '@rollup/rollup-darwin-arm64': 4.0.2 + '@rollup/rollup-darwin-x64': 4.0.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.0.2 + '@rollup/rollup-linux-arm64-gnu': 4.0.2 + '@rollup/rollup-linux-arm64-musl': 4.0.2 + '@rollup/rollup-linux-x64-gnu': 4.0.2 + '@rollup/rollup-linux-x64-musl': 4.0.2 + '@rollup/rollup-win32-arm64-msvc': 4.0.2 + '@rollup/rollup-win32-ia32-msvc': 4.0.2 + '@rollup/rollup-win32-x64-msvc': 4.0.2 + fsevents: 2.3.2 + /rrweb-cssom@0.6.0: resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} dev: false @@ -17196,10 +17214,6 @@ packages: isarray: 2.0.5 dev: true - /safe-buffer@5.1.1: - resolution: {integrity: sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==} - dev: true - /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -17239,8 +17253,8 @@ packages: postcss: 8.4.31 dev: false - /sass@1.68.0: - resolution: {integrity: sha512-Lmj9lM/fef0nQswm1J2HJcEsBUba4wgNx2fea6yJHODREoMFnwRpZydBnX/RjyXw2REIwdkbqE4hrTo4qfDBUA==} + /sass@1.69.1: + resolution: {integrity: sha512-nc969GvTVz38oqKgYYVHM/Iq7Yl33IILy5uqaH2CWSiSUmRCvw+UR7tA3845Sp4BD5ykCUimvrT3k1EjTwpVUA==} engines: {node: '>=14.0.0'} hasBin: true dependencies: @@ -17321,17 +17335,6 @@ packages: transitivePeerDependencies: - supports-color - /serve-favicon@2.5.0: - resolution: {integrity: sha512-FMW2RvqNr03x+C0WxTyu6sOv21oOjkq5j8tjquWccwa6ScNyGFOGJVpuS1NmTVGBAHS07xnSKotgf2ehQmf9iA==} - engines: {node: '>= 0.8.0'} - dependencies: - etag: 1.8.1 - fresh: 0.5.2 - ms: 2.1.1 - parseurl: 1.3.3 - safe-buffer: 5.1.1 - dev: true - /serve-static@1.15.0: resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} engines: {node: '>= 0.8.0'} @@ -17846,11 +17849,11 @@ packages: resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} dev: true - /storybook@7.4.5: - resolution: {integrity: sha512-J7fidphTJ6SJHlR8f/USQE30K6ipbynLVLsTOz0bNYW/0Ua2t9u6dAYGbbq6bLikl3zxzQbdm9lXMUzmaYAdIA==} + /storybook@7.4.6: + resolution: {integrity: sha512-YkFSpnR47j5zz7yElA+2axLjXN7K7TxDGJRHHlqXmG5iQ0PXzmjrj2RxMDKFz4Ybp/QjEUoJ4rx//ESEY0Nb5A==} hasBin: true dependencies: - '@storybook/cli': 7.4.5 + '@storybook/cli': 7.4.6 transitivePeerDependencies: - bufferutil - encoding @@ -18122,8 +18125,8 @@ packages: resolution: {integrity: sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==} dev: true - /systeminformation@5.21.9: - resolution: {integrity: sha512-7pI4mu9P/2MGDV0T49B52E7IULBGj+kRVk6JSYUj5qfAk7N7C7aNX15fXziqrbgZntc6/jjYzWeb/x41jhg/eA==} + /systeminformation@5.21.11: + resolution: {integrity: sha512-dIJEGoP5W7k4JJGje/b+inJrOL5hV9LPsUi5ndBvJydI80CVEcu2DZYgt6prdRErDi2SA4SqYd/WMR4b+u34mA==} engines: {node: '>=8.0.0'} os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true @@ -18216,8 +18219,8 @@ packages: unique-string: 2.0.0 dev: true - /terser@5.20.0: - resolution: {integrity: sha512-e56ETryaQDyebBwJIWYB2TT6f2EZ0fL0sW/JRXNMN26zZdKi2u/E/5my5lG6jNxym6qsrVXfFRmOdV42zlAgLQ==} + /terser@5.21.0: + resolution: {integrity: sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw==} engines: {node: '>=10'} hasBin: true dependencies: @@ -18751,6 +18754,9 @@ packages: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} dev: true + /undici-types@5.25.3: + resolution: {integrity: sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==} + /undici@5.22.1: resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==} engines: {node: '>=14.0'} @@ -19020,8 +19026,8 @@ packages: core-util-is: 1.0.2 extsprintf: 1.3.0 - /vite-node@0.34.5(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0): - resolution: {integrity: sha512-RNZ+DwbCvDoI5CbCSQSyRyzDTfFvFauvMs6Yq4ObJROKlIKuat1KgSX/Ako5rlDMfVCyMcpMRMTkJBxd6z8YRA==} + /vite-node@0.34.6(@types/node@20.8.4)(sass@1.69.1)(terser@5.21.0): + resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} engines: {node: '>=v14.18.0'} hasBin: true dependencies: @@ -19030,7 +19036,7 @@ packages: mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.4)(sass@1.69.1)(terser@5.21.0) transitivePeerDependencies: - '@types/node' - less @@ -19046,8 +19052,8 @@ packages: resolution: {integrity: sha512-p4D8CFVhZS412SyQX125qxyzOgIFouwOcvjZWk6bQbNPR1wtaEzFT6jZxAjf1dejlGqa6fqHcuCvQea6EWUkUA==} dev: true - /vite@4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0): - resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} + /vite@4.4.11(@types/node@20.8.4)(sass@1.69.1)(terser@5.21.0): + resolution: {integrity: sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: @@ -19074,29 +19080,29 @@ packages: terser: optional: true dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.4 esbuild: 0.18.17 postcss: 8.4.31 rollup: 3.29.4 - sass: 1.68.0 - terser: 5.20.0 + sass: 1.69.1 + terser: 5.21.0 optionalDependencies: fsevents: 2.3.2 - /vitest-fetch-mock@0.2.2(vitest@0.34.5): + /vitest-fetch-mock@0.2.2(vitest@0.34.6): resolution: {integrity: sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==} engines: {node: '>=14.14.0'} peerDependencies: vitest: '>=0.16.0' dependencies: cross-fetch: 3.1.5 - vitest: 0.34.5(happy-dom@10.0.3)(sass@1.68.0)(terser@5.20.0) + vitest: 0.34.6(happy-dom@10.0.3)(sass@1.69.1)(terser@5.21.0) transitivePeerDependencies: - encoding dev: true - /vitest@0.34.5(happy-dom@10.0.3)(sass@1.68.0)(terser@5.20.0): - resolution: {integrity: sha512-CPI68mmnr2DThSB3frSuE5RLm9wo5wU4fbDrDwWQQB1CWgq9jQVoQwnQSzYAjdoBOPoH2UtXpOgHVge/uScfZg==} + /vitest@0.34.6(happy-dom@10.0.3)(sass@1.69.1)(terser@5.21.0): + resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} engines: {node: '>=v14.18.0'} hasBin: true peerDependencies: @@ -19128,16 +19134,16 @@ packages: dependencies: '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 - '@types/node': 20.7.1 - '@vitest/expect': 0.34.5 - '@vitest/runner': 0.34.5 - '@vitest/snapshot': 0.34.5 - '@vitest/spy': 0.34.5 - '@vitest/utils': 0.34.5 + '@types/node': 20.8.4 + '@vitest/expect': 0.34.6 + '@vitest/runner': 0.34.6 + '@vitest/snapshot': 0.34.6 + '@vitest/spy': 0.34.6 + '@vitest/utils': 0.34.6 acorn: 8.10.0 acorn-walk: 8.2.0 cac: 6.7.14 - chai: 4.3.7 + chai: 4.3.10 debug: 4.3.4(supports-color@8.1.1) happy-dom: 10.0.3 local-pkg: 0.4.3 @@ -19148,8 +19154,8 @@ packages: strip-literal: 1.0.1 tinybench: 2.5.0 tinypool: 0.7.0 - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) - vite-node: 0.34.5(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.4)(sass@1.69.1)(terser@5.21.0) + vite-node: 0.34.6(@types/node@20.8.4)(sass@1.69.1)(terser@5.21.0) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -19165,8 +19171,8 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} - /vue-component-type-helpers@1.8.15: - resolution: {integrity: sha512-RKiPRKW4BdwgmQ9vaNkHYKAThdTbgU4TOphVyyzqxRwsOJOoRIrb+vB49XLvs5CKPNrvxMXZMwPe5FyJCqFWyg==} + /vue-component-type-helpers@1.8.18: + resolution: {integrity: sha512-SklLIg782E5Ff0qdE68AUrRBhT2YGW97edBewNEjCWCw+RSETcGOjA8m1/6T68CXkymWBSk+KDpPXqIGthqCDg==} dev: true /vue-demi@0.13.11(vue@3.3.4): @@ -19202,17 +19208,17 @@ packages: - vue dev: true - /vue-eslint-parser@9.3.1(eslint@8.50.0): - resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==} + /vue-eslint-parser@9.3.2(eslint@8.51.0): + resolution: {integrity: sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: debug: 4.3.4(supports-color@8.1.1) - eslint: 8.50.0 - eslint-scope: 7.2.0 - eslint-visitor-keys: 3.4.1 - espree: 9.5.2 + eslint: 8.51.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 esquery: 1.4.2 lodash: 4.17.21 semver: 7.5.4 @@ -19244,14 +19250,14 @@ packages: he: 1.2.0 dev: true - /vue-tsc@1.8.15(typescript@5.2.2): - resolution: {integrity: sha512-4DoB3LUj7IToLmggoCxRiFG+QU5lem0nv03m1ocqugXA9rSVoTOEoYYaP8vu8b99Eh+/cCVdYOeIAQ+RsgUYUw==} + /vue-tsc@1.8.18(typescript@5.2.2): + resolution: {integrity: sha512-AwQxBB9SZX308TLL1932P1JByuMsXC2jLfRBGt8SBdm1e3cXkDlFaXUAqibfKnoQ1ZC2zO2NSbeBNdSjOcdvJw==} hasBin: true peerDependencies: typescript: '*' dependencies: - '@vue/language-core': 1.8.15(typescript@5.2.2) - '@vue/typescript': 1.8.15(typescript@5.2.2) + '@vue/language-core': 1.8.18(typescript@5.2.2) + '@vue/typescript': 1.8.18(typescript@5.2.2) semver: 7.5.4 typescript: 5.2.2 dev: true @@ -19707,7 +19713,7 @@ packages: sharp: 0.31.3 dev: false - github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.4.5)(@storybook/components@7.4.4)(@storybook/core-events@7.4.5)(@storybook/manager-api@7.4.5)(@storybook/preview-api@7.4.5)(@storybook/theming@7.4.5)(@storybook/types@7.4.5)(react-dom@18.2.0)(react@18.2.0): + github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.4.6)(@storybook/components@7.4.6)(@storybook/core-events@7.4.6)(@storybook/manager-api@7.4.6)(@storybook/preview-api@7.4.6)(@storybook/theming@7.4.6)(@storybook/types@7.4.6)(react-dom@18.2.0)(react@18.2.0): resolution: {tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640} id: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640 name: storybook-addon-misskey-theme @@ -19728,13 +19734,13 @@ packages: react-dom: optional: true dependencies: - '@storybook/blocks': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/components': 7.4.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/blocks': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true