Compare commits

...

27 Commits

Author SHA1 Message Date
かっこかり f10ad5a556
Merge branch 'develop' into external-resources 2023-10-19 20:54:04 +09:00
syuilo 4a7c6e261a fix(backend): 管理者権限のロールを持っていても一部のAPIが使用できないことがある問題を修正 2023-10-19 20:47:23 +09:00
かっこかり 48cf4e6382
Merge branch 'develop' into external-resources 2023-10-19 20:36:07 +09:00
syuilo e5598da7a2 disable cypress widgets tests 2023-10-19 20:22:24 +09:00
syuilo cc256f117e update deps 2023-10-19 19:51:59 +09:00
syuilo d9241df84d
New Crowdin updates (#12070)
* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Chinese Traditional)
2023-10-19 19:47:36 +09:00
syuilo 84a9e4a27b
Update CHANGELOG.md 2023-10-19 19:47:25 +09:00
atsuchan 7b361224f8
fix(frontend): Recieve Unrenote on streaming (#12079)
* fix(frontend): Recieve Unrenote

表示しているリノートがリノート解除されたらストリーミングで受信してすぐに消えるようにする

* fix(frontend): Recieve Unrenote lint fixing

* fix(frontend): Recieve Unrenote Decapture

Decapture忘れてたー
2023-10-19 19:36:18 +09:00
dependabot[bot] 3c3d05ba2e
chore(deps): bump actions/checkout from 4.1.0 to 4.1.1 (#12062)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 19:35:13 +09:00
anatawa12 991fa054a6
chore: STLのdb fallbackでwithRepliesがtrueのときにすべてのリプライを除外しないように (#12075)
MiFollowingを見るのは実装コストが高いため現状実装していない

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-10-19 19:34:52 +09:00
A.Yamamoto 9afcdd10ed
UserLite.nameにnullが入りうるのを型で明示 (#12073)
* UserLite.nameにnullが入りうるのを型で明示

* ドキュメントの追従
2023-10-19 19:33:45 +09:00
かっこかり bad6e00395
Merge branch 'develop' into external-resources 2023-10-19 18:09:55 +09:00
syuilo 721cbe085b fix(frontend): fix of 30efd932a5 2023-10-19 17:42:19 +09:00
anatawa12 93d3501c90
fix: replies are included even if withReplies = false in local timeline (#12074) 2023-10-19 17:29:09 +09:00
かっこかり 6cd0d6c9ed
Merge branch 'develop' into external-resources 2023-10-19 16:41:02 +09:00
syuilo 431d8c7802 fix(backend): Redisがからのときにhybrid-timelineにwithReplies = trueでアクセスするとSQLのシンタックスエラーになる
Fix #12064
2023-10-19 16:22:19 +09:00
かっこかり c219c555e7
Merge branch 'develop' into external-resources 2023-10-19 14:36:14 +09:00
syuilo f85a655915 2023.10.2-beta.2 2023-10-19 11:43:28 +09:00
syuilo 5891adc5cf Update CHANGELOG.md 2023-10-19 11:42:52 +09:00
syuilo 30efd932a5 enhance: nyaizeはクライアントで表示時に行うように
Resolve #12030
2023-10-19 11:42:17 +09:00
syuilo ec45db7870 refactor and perf tweak 2023-10-19 11:19:42 +09:00
syuilo 428d39a460 chore: disable debug log of fastify 2023-10-19 11:18:17 +09:00
syuilo f9549e1f1b fix(backend): fix of 1671575d5d 2023-10-19 11:17:59 +09:00
syuilo 1671575d5d perf(backend): ノートのリアクション情報をキャッシュすることでDBへのクエリを削減 2023-10-19 09:20:19 +09:00
syuilo 4d1d25e02f perf(backend): improve my reaction population performance 2023-10-19 08:07:22 +09:00
syuilo 2dfbf97db4 refactor 2023-10-19 07:59:58 +09:00
syuilo fcc4864080 perf(backend): reduce needless populateMyReaction calls 2023-10-19 07:56:25 +09:00
50 changed files with 695 additions and 605 deletions

View File

@ -9,7 +9,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.0 uses: actions/checkout@v4.1.1
- run: corepack enable - run: corepack enable

View File

@ -10,7 +10,7 @@ jobs:
check_copyright_year: check_copyright_year:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4.1.0 - uses: actions/checkout@v4.1.1
- run: | - run: |
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
echo "Please change copyright year!" echo "Please change copyright year!"

View File

@ -13,7 +13,7 @@ jobs:
if: github.repository == 'misskey-dev/misskey' if: github.repository == 'misskey-dev/misskey'
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v4.1.0 uses: actions/checkout@v4.1.1
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v3.0.0 uses: docker/setup-buildx-action@v3.0.0

View File

@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v4.1.0 uses: actions/checkout@v4.1.1
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v3.0.0 uses: docker/setup-buildx-action@v3.0.0

View File

@ -14,7 +14,7 @@ jobs:
env: env:
DOCKER_CONTENT_TRUST: 1 DOCKER_CONTENT_TRUST: 1
steps: steps:
- uses: actions/checkout@v4.1.0 - uses: actions/checkout@v4.1.1
- run: | - run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb" curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
sudo dpkg -i dockle.deb sudo dpkg -i dockle.deb

View File

@ -11,7 +11,7 @@ jobs:
pnpm_install: pnpm_install:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4.1.0 - uses: actions/checkout@v4.1.1
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
@ -38,7 +38,7 @@ jobs:
- sw - sw
- misskey-js - misskey-js
steps: steps:
- uses: actions/checkout@v4.1.0 - uses: actions/checkout@v4.1.1
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
@ -64,7 +64,7 @@ jobs:
- backend - backend
- misskey-js - misskey-js
steps: steps:
- uses: actions/checkout@v4.1.0 - uses: actions/checkout@v4.1.1
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true

View File

@ -53,7 +53,7 @@ jobs:
# Check out merge commit # Check out merge commit
- name: Fork based /deploy checkout - name: Fork based /deploy checkout
uses: actions/checkout@v4.1.0 uses: actions/checkout@v4.1.1
with: with:
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'

View File

@ -29,7 +29,7 @@ jobs:
- 56312:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v4.1.0 - uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm

View File

@ -16,7 +16,7 @@ jobs:
node-version: [20.5.1] node-version: [20.5.1]
steps: steps:
- uses: actions/checkout@v4.1.0 - uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm
@ -68,7 +68,7 @@ jobs:
- 56312:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v4.1.0 - uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
# https://github.com/cypress-io/cypress-docker-images/issues/150 # https://github.com/cypress-io/cypress-docker-images/issues/150

View File

@ -21,7 +21,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.0 uses: actions/checkout@v4.1.1
- run: corepack enable - run: corepack enable

View File

@ -19,7 +19,7 @@ jobs:
node-version: [20.5.1] node-version: [20.5.1]
steps: steps:
- uses: actions/checkout@v4.1.0 - uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm

View File

@ -18,7 +18,7 @@
- Feat: アンテナでローカルの投稿のみ収集できるようになりました - Feat: アンテナでローカルの投稿のみ収集できるようになりました
- Feat: サーバーサイレンス機能が追加されました - Feat: サーバーサイレンス機能が追加されました
- Enhance: 新規にフォローした人の返信をデフォルトでTLに追加できるオプションを追加 - Enhance: 新規にフォローした人の返信をデフォルトでTLに追加できるオプションを追加
- Enhance: HTLとLTLを2023.10.0アップデート以前まで遡れるように - Enhance: HTL/LTL/STLを2023.10.0アップデート以前まで遡れるように
- Enhance: フォロー/フォロー解除したときに過去分のHTLにも含まれる投稿が反映されるように - Enhance: フォロー/フォロー解除したときに過去分のHTLにも含まれる投稿が反映されるように
- Enhance: ローカリゼーションの更新 - Enhance: ローカリゼーションの更新
- Enhance: 依存関係の更新 - Enhance: 依存関係の更新
@ -30,10 +30,13 @@
https://misskey-hub.net/docs/advanced/publish-on-your-website.html https://misskey-hub.net/docs/advanced/publish-on-your-website.html
### Server ### Server
- Enhance: タイムライン取得時のパフォーマンスを向上
- Enhance: ストリーミングAPIのパフォーマンスを向上 - Enhance: ストリーミングAPIのパフォーマンスを向上
- Fix: users/notesでDBから参照した際にチャンネル投稿のみ取得される問題を修正 - Fix: users/notesでDBから参照した際にチャンネル投稿のみ取得される問題を修正
- Fix: コントロールパネルの設定項目が正しく保存できない問題を修正 - Fix: コントロールパネルの設定項目が正しく保存できない問題を修正
- Change: nyaizeはAPIレスポンス時ではなく投稿時に一度だけ非可逆的に行われるようになりました - Fix: 管理者権限のロールを持っていても一部のAPIが使用できないことがある問題を修正
- Change: ユーザーのisCatがtrueでも、サーバーではnyaizeが行われなくなりました
- isCatな場合、クライアントでnyaize処理を行うことを推奨します
## 2023.10.1 ## 2023.10.1
### General ### General

View File

@ -1,3 +1,4 @@
/* flaky
describe('After user signed in', () => { describe('After user signed in', () => {
beforeEach(() => { beforeEach(() => {
cy.resetState(); cy.resetState();
@ -67,3 +68,4 @@ describe('After user signed in', () => {
buildWidgetTest('aiscript'); buildWidgetTest('aiscript');
buildWidgetTest('aichan'); buildWidgetTest('aichan');
}); });
*/

View File

@ -593,7 +593,7 @@ poll: "Poll"
useCw: "Hide content" useCw: "Hide content"
enablePlayer: "Open video player" enablePlayer: "Open video player"
disablePlayer: "Close video player" disablePlayer: "Close video player"
expandTweet: "Expand tweet" expandTweet: "Expand post"
themeEditor: "Theme editor" themeEditor: "Theme editor"
description: "Description" description: "Description"
describeFile: "Add caption" describeFile: "Add caption"

View File

@ -387,7 +387,7 @@ antennaSource: "Source de lantenne"
antennaKeywords: "Mots clés à recevoir" antennaKeywords: "Mots clés à recevoir"
antennaExcludeKeywords: "Mots clés à exclure" antennaExcludeKeywords: "Mots clés à exclure"
antennaKeywordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR." antennaKeywordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR."
notifyAntenna: "Je souhaite recevoir les notifications des nouvelles notes" notifyAntenna: "Me notifier pour les nouvelles notes"
withFileAntenna: "Notes ayant des attachements uniquement" withFileAntenna: "Notes ayant des attachements uniquement"
enableServiceworker: "Activer ServiceWorker" enableServiceworker: "Activer ServiceWorker"
antennaUsersDescription: "Saisissez un seul nom dutilisateur·rice par ligne" antennaUsersDescription: "Saisissez un seul nom dutilisateur·rice par ligne"
@ -985,11 +985,13 @@ internalServerError: "Erreur interne du serveur"
copyErrorInfo: "Copier les détails de lerreur" copyErrorInfo: "Copier les détails de lerreur"
exploreOtherServers: "Trouver une autre instance" exploreOtherServers: "Trouver une autre instance"
disableFederationOk: "Désactiver" disableFederationOk: "Désactiver"
postToTheChannel: "Publier au canal"
likeOnly: "Les favoris uniquement" likeOnly: "Les favoris uniquement"
sensitiveWords: "Mots sensibles" sensitiveWords: "Mots sensibles"
notesSearchNotAvailable: "La recherche de notes n'est pas disponible." notesSearchNotAvailable: "La recherche de notes n'est pas disponible."
license: "Licence" license: "Licence"
myClips: "Mes clips" myClips: "Mes clips"
retryAllQueuesConfirmText: "Cela peut augmenter temporairement la charge du serveur."
showClipButtonInNoteFooter: "Ajouter « Clip » au menu d'action de la note" showClipButtonInNoteFooter: "Ajouter « Clip » au menu d'action de la note"
noteIdOrUrl: "Identifiant de la note ou URL" noteIdOrUrl: "Identifiant de la note ou URL"
video: "Vidéo" video: "Vidéo"
@ -1013,21 +1015,30 @@ vertical: "Vertical"
horizontal: "Latéral" horizontal: "Latéral"
position: "Position" position: "Position"
serverRules: "Règles du serveur" serverRules: "Règles du serveur"
preservedUsernames: "Nom d'utilisateur·rice réservé" pleaseAgreeAllToContinue: "Pour continuer, veuillez accepter tous les champs ci-dessus."
continue: "Continuer"
preservedUsernames: "Noms d'utilisateur·rice réservés"
archive: "Archive" archive: "Archive"
displayOfNote: "Affichage de la note" displayOfNote: "Affichage de la note"
initialAccountSetting: "Réglage initial du profil"
youFollowing: "Abonné·e" youFollowing: "Abonné·e"
preventAiLearning: "Refuser l'usage dans l'apprentissage automatique d'IA générative"
options: "Options" options: "Options"
later: "Plus tard" later: "Plus tard"
goToMisskey: "Retour vers Misskey" goToMisskey: "Retour vers Misskey"
expirationDate: "Date dexpiration" expirationDate: "Date dexpiration"
waitingForMailAuth: "En attente de la vérification de l'adresse courriel"
usedAt: "Utilisé le" usedAt: "Utilisé le"
unused: "Non-utilisé" unused: "Non-utilisé"
used: "Utilisé" used: "Utilisé"
expired: "Expiré" expired: "Expiré"
doYouAgree: "Êtes-vous daccord ?" doYouAgree: "Êtes-vous daccord ?"
beSureToReadThisAsItIsImportant: "Assurez-vous de le lire; c'est important."
dialog: "Dialogue"
icon: "Avatar" icon: "Avatar"
forYou: "Pour vous" forYou: "Pour vous"
currentAnnouncements: "Annonces actuelles"
pastAnnouncements: "Annonces passées"
replies: "Répondre" replies: "Répondre"
renotes: "Renoter" renotes: "Renoter"
loadReplies: "Inclure les réponses" loadReplies: "Inclure les réponses"

View File

@ -110,7 +110,7 @@ unrenote: "Elimina la Rinota"
renoted: "Rinotato!" renoted: "Rinotato!"
cantRenote: "È impossibile rinotare questa nota." cantRenote: "È impossibile rinotare questa nota."
cantReRenote: "È impossibile rinotare una Rinota." cantReRenote: "È impossibile rinotare una Rinota."
quote: "Cita" quote: "Citazione"
inChannelRenote: "Rinota nel canale" inChannelRenote: "Rinota nel canale"
inChannelQuote: "Cita nel canale" inChannelQuote: "Cita nel canale"
pinnedNote: "Nota in primo piano" pinnedNote: "Nota in primo piano"
@ -534,6 +534,7 @@ serverLogs: "Log del server"
deleteAll: "Cancella cronologia" deleteAll: "Cancella cronologia"
showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline" showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline"
showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline" showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline"
withRepliesByDefaultForNewlyFollowed: "Quando segui nuovi profili, includi le risposte in TL come impostazione predefinita"
newNoteRecived: "Nuove note da leggere" newNoteRecived: "Nuove note da leggere"
sounds: "Impostazioni suoni" sounds: "Impostazioni suoni"
sound: "Suono" sound: "Suono"
@ -1924,6 +1925,7 @@ _exportOrImport:
userLists: "Liste" userLists: "Liste"
excludeMutingUsers: "Escludere gli utenti silenziati" excludeMutingUsers: "Escludere gli utenti silenziati"
excludeInactiveUsers: "Escludere i profili inutilizzati" excludeInactiveUsers: "Escludere i profili inutilizzati"
withReplies: "Includere le risposte da profili importati nella Timeline"
_charts: _charts:
federation: "Federazione" federation: "Federazione"
apRequest: "Richieste" apRequest: "Richieste"
@ -2088,7 +2090,7 @@ _deck:
list: "Liste" list: "Liste"
channel: "Canale" channel: "Canale"
mentions: "Menzioni" mentions: "Menzioni"
direct: "Diretta" direct: "Note Dirette"
roleTimeline: "Timeline Ruolo" roleTimeline: "Timeline Ruolo"
_dialog: _dialog:
charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})" charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})"

View File

@ -586,7 +586,7 @@ poll: "アンケート"
useCw: "内容を隠す" useCw: "内容を隠す"
enablePlayer: "プレイヤーを開く" enablePlayer: "プレイヤーを開く"
disablePlayer: "プレイヤーを閉じる" disablePlayer: "プレイヤーを閉じる"
expandTweet: "ツイートを展開する" expandTweet: "ポストを展開する"
themeEditor: "テーマエディター" themeEditor: "テーマエディター"
description: "説明" description: "説明"
describeFile: "キャプションを付ける" describeFile: "キャプションを付ける"

View File

@ -589,7 +589,7 @@ poll: "투표"
useCw: "내용 숨기기" useCw: "내용 숨기기"
enablePlayer: "플레이어 열기" enablePlayer: "플레이어 열기"
disablePlayer: "플레이어 닫기" disablePlayer: "플레이어 닫기"
expandTweet: "트윗 확장하기" expandTweet: "게시물 확장하기"
themeEditor: "테마 에디터" themeEditor: "테마 에디터"
description: "설명" description: "설명"
describeFile: "캡션 추가" describeFile: "캡션 추가"

View File

@ -1747,8 +1747,8 @@ _2fa:
renewTOTPOk: "ตั้งค่าคอนฟิกใหม่" renewTOTPOk: "ตั้งค่าคอนฟิกใหม่"
renewTOTPCancel: "ไม่เป็นไร" renewTOTPCancel: "ไม่เป็นไร"
backupCodes: "รหัสสำรองข้อมูล" backupCodes: "รหัสสำรองข้อมูล"
backupCodeUsedWarning: "มีการใช้รหัสสำรองแล้ว โปรดกรุณากำหนดค่าการตรวจสอบสิทธิ์แบบสองปัจจัยโดยเร็วที่สุดถ้าหากคุณยังไม่สามารถใช้งานได้อีกต่อไป" backupCodeUsedWarning: "มีการใช้รหัสสำรองแล้ว โปรดกรุณากำหนดค่าการตรวจสอบสิทธิ์แบบสองปัจจัยโดยเร็วที่สุดถ้าหากคุณยังไม่สามารถใช้งานได้อีก"
backupCodesExhaustedWarning: "รหัสสำรองทั้งหมดถูกใช้แล้วถ้าหากคุณยังสูญเสียการเข้าถึงแอปการตรวจสอบสิทธิ์แบบสองปัจจัยคุณจะไม่สามารถเข้าถึงบัญชีนี้ได้ กรุณากำหนดค่าการรับรองความถูกต้องด้วยการยืนยันสองชั้น" backupCodesExhaustedWarning: "รหัสสำรองทั้งหมดถูกใช้แล้ว ถ้าหากคุณยังสูญเสียการเข้าถึงแอปการตรวจสอบสิทธิ์แบบสองปัจจัยคุณจะยังไม่สามารถเข้าถึงบัญชีนี้ได้ กรุณากำหนดค่าการรับรองความถูกต้องด้วยการยืนยันสองชั้น"
_permissions: _permissions:
"read:account": "ดูข้อมูลบัญชีของคุณ" "read:account": "ดูข้อมูลบัญชีของคุณ"
"write:account": "แก้ไขข้อมูลบัญชีของคุณ" "write:account": "แก้ไขข้อมูลบัญชีของคุณ"

View File

@ -195,6 +195,7 @@ perHour: "每小時"
perDay: "每日" perDay: "每日"
stopActivityDelivery: "停止發送活動" stopActivityDelivery: "停止發送活動"
blockThisInstance: "封鎖此伺服器" blockThisInstance: "封鎖此伺服器"
silenceThisInstance: "禁言此伺服器"
operations: "操作" operations: "操作"
software: "軟體" software: "軟體"
version: "版本" version: "版本"
@ -214,6 +215,8 @@ clearCachedFiles: "清除快取資料"
clearCachedFilesConfirm: "確定要清除所有遠端暫存資料嗎?" clearCachedFilesConfirm: "確定要清除所有遠端暫存資料嗎?"
blockedInstances: "已封鎖的伺服器" blockedInstances: "已封鎖的伺服器"
blockedInstancesDescription: "請逐行輸入需要封鎖的伺服器。已封鎖的伺服器將無法與本伺服器進行通訊。" blockedInstancesDescription: "請逐行輸入需要封鎖的伺服器。已封鎖的伺服器將無法與本伺服器進行通訊。"
silencedInstances: "被禁言的伺服器"
silencedInstancesDescription: "設定要禁言的伺服器主機名稱,以換行分隔。隸屬於禁言伺服器的所有帳戶都將被視為「禁言帳戶」,只能發出「追隨請求」,而且無法提及未追隨的本地帳戶。這不會影響已封鎖的實例。"
muteAndBlock: "靜音和封鎖" muteAndBlock: "靜音和封鎖"
mutedUsers: "被靜音的使用者" mutedUsers: "被靜音的使用者"
blockedUsers: "被封鎖的使用者" blockedUsers: "被封鎖的使用者"
@ -531,6 +534,7 @@ serverLogs: "伺服器日誌"
deleteAll: "刪除所有記錄" deleteAll: "刪除所有記錄"
showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框" showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框"
showFixedPostFormInChannel: "於時間軸頁頂顯示「發送貼文」方框(頻道)" showFixedPostFormInChannel: "於時間軸頁頂顯示「發送貼文」方框(頻道)"
withRepliesByDefaultForNewlyFollowed: "在追隨其他人後,預設在時間軸納入回覆的貼文"
newNoteRecived: "發現新貼文" newNoteRecived: "發現新貼文"
sounds: "音效" sounds: "音效"
sound: "音效" sound: "音效"
@ -1921,6 +1925,7 @@ _exportOrImport:
userLists: "清單" userLists: "清單"
excludeMutingUsers: "排除被靜音的使用者" excludeMutingUsers: "排除被靜音的使用者"
excludeInactiveUsers: "排除不活躍帳戶" excludeInactiveUsers: "排除不活躍帳戶"
withReplies: "將被匯入的追隨中清單的貼文回覆包含在時間軸"
_charts: _charts:
federation: "聯邦宇宙" federation: "聯邦宇宙"
apRequest: "請求" apRequest: "請求"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2023.10.2-beta.1", "version": "2023.10.2-beta.2",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@ -47,14 +47,14 @@
"cssnano": "6.0.1", "cssnano": "6.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"postcss": "8.4.31", "postcss": "8.4.31",
"terser": "5.21.0", "terser": "5.22.0",
"typescript": "5.2.2" "typescript": "5.2.2"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "6.8.0", "@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0", "@typescript-eslint/parser": "6.8.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.3.1", "cypress": "13.3.2",
"eslint": "8.51.0", "eslint": "8.51.0",
"start-server-and-test": "2.0.1" "start-server-and-test": "2.0.1"
}, },

View File

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class NoteReactionAndUserPairCache1697673894459 {
name = 'NoteReactionAndUserPairCache1697673894459'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "reactionAndUserPairCache" character varying(1024) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "reactionAndUserPairCache"`);
}
}

View File

@ -76,7 +76,7 @@
"@nestjs/testing": "10.2.7", "@nestjs/testing": "10.2.7",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "8.3.2", "@simplewebauthn/server": "8.3.2",
"@sinonjs/fake-timers": "11.1.0", "@sinonjs/fake-timers": "11.2.1",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.93", "@swc/core": "1.3.93",
"accepts": "1.3.8", "accepts": "1.3.8",
@ -86,7 +86,7 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"bullmq": "4.12.4", "bullmq": "4.12.5",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.1", "cbor": "9.0.1",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -97,7 +97,7 @@
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify": "4.24.2", "fastify": "4.24.3",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.5.0", "file-type": "18.5.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
@ -180,38 +180,38 @@
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
"@types/color-convert": "2.0.2", "@types/color-convert": "2.0.2",
"@types/content-disposition": "0.5.7", "@types/content-disposition": "0.5.7",
"@types/fluent-ffmpeg": "2.1.22", "@types/fluent-ffmpeg": "2.1.23",
"@types/http-link-header": "1.0.3", "@types/http-link-header": "1.0.4",
"@types/jest": "29.5.5", "@types/jest": "29.5.6",
"@types/js-yaml": "4.0.7", "@types/js-yaml": "4.0.8",
"@types/jsdom": "21.1.3", "@types/jsdom": "21.1.4",
"@types/jsonld": "1.5.10", "@types/jsonld": "1.5.11",
"@types/jsrsasign": "10.5.9", "@types/jsrsasign": "10.5.10",
"@types/mime-types": "2.1.2", "@types/mime-types": "2.1.3",
"@types/ms": "0.7.32", "@types/ms": "0.7.33",
"@types/node": "20.8.6", "@types/node": "20.8.7",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.11", "@types/nodemailer": "6.4.13",
"@types/oauth": "0.9.2", "@types/oauth": "0.9.3",
"@types/oauth2orize": "1.11.1", "@types/oauth2orize": "1.11.2",
"@types/oauth2orize-pkce": "0.1.0", "@types/oauth2orize-pkce": "0.1.1",
"@types/pg": "8.10.5", "@types/pg": "8.10.7",
"@types/pug": "2.0.7", "@types/pug": "2.0.8",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.1",
"@types/qrcode": "1.5.2", "@types/qrcode": "1.5.4",
"@types/random-seed": "0.3.3", "@types/random-seed": "0.3.4",
"@types/ratelimiter": "3.4.4", "@types/ratelimiter": "3.4.5",
"@types/rename": "1.0.5", "@types/rename": "1.0.6",
"@types/sanitize-html": "2.9.2", "@types/sanitize-html": "2.9.3",
"@types/semver": "7.5.3", "@types/semver": "7.5.4",
"@types/sharp": "0.32.0", "@types/sharp": "0.32.0",
"@types/simple-oauth2": "5.0.5", "@types/simple-oauth2": "5.0.6",
"@types/sinonjs__fake-timers": "8.1.3", "@types/sinonjs__fake-timers": "8.1.4",
"@types/tinycolor2": "1.4.4", "@types/tinycolor2": "1.4.5",
"@types/tmp": "0.2.4", "@types/tmp": "0.2.5",
"@types/vary": "1.1.1", "@types/vary": "1.1.2",
"@types/web-push": "3.6.1", "@types/web-push": "3.6.2",
"@types/ws": "8.5.7", "@types/ws": "8.5.8",
"@typescript-eslint/eslint-plugin": "6.8.0", "@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0", "@typescript-eslint/parser": "6.8.0",
"aws-sdk-client-mock": "3.0.0", "aws-sdk-client-mock": "3.0.0",

View File

@ -227,8 +227,6 @@ export class NoteCreateService implements OnApplicationShutdown {
isBot: MiUser['isBot']; isBot: MiUser['isBot'];
isCat: MiUser['isCat']; isCat: MiUser['isCat'];
}, data: Option, silent = false): Promise<MiNote> { }, data: Option, silent = false): Promise<MiNote> {
let patsedText: mfm.MfmNode[] | null = null;
// チャンネル外にリプライしたら対象のスコープに合わせる // チャンネル外にリプライしたら対象のスコープに合わせる
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで) // (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
if (data.reply && data.channel && data.reply.channelId !== data.channel.id) { if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
@ -315,25 +313,6 @@ export class NoteCreateService implements OnApplicationShutdown {
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
} }
data.text = data.text.trim(); data.text = data.text.trim();
if (user.isCat) {
patsedText = mfm.parse(data.text);
function nyaizeNode(node: mfm.MfmNode) {
if (node.type === 'quote') return;
if (node.type === 'text') {
node.props.text = nyaize(node.props.text);
}
if (node.children) {
for (const child of node.children) {
nyaizeNode(child);
}
}
}
for (const node of patsedText) {
nyaizeNode(node);
}
data.text = mfm.toString(patsedText);
}
} else { } else {
data.text = null; data.text = null;
} }
@ -344,7 +323,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// Parse MFM if needed // Parse MFM if needed
if (!tags || !emojis || !mentionedUsers) { if (!tags || !emojis || !mentionedUsers) {
const tokens = patsedText ?? (data.text ? mfm.parse(data.text)! : []); const tokens = (data.text ? mfm.parse(data.text)! : []);
const cwTokens = data.cw ? mfm.parse(data.cw)! : []; const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
const choiceTokens = data.poll && data.poll.choices const choiceTokens = data.poll && data.poll.choices
? concat(data.poll.choices.map(choice => mfm.parse(choice)!)) ? concat(data.poll.choices.map(choice => mfm.parse(choice)!))
@ -584,7 +563,7 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
// Pack the note // Pack the note
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true }); const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });
this.globalEventService.publishNotesStream(noteObj); this.globalEventService.publishNotesStream(noteObj);

View File

@ -30,6 +30,7 @@ import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js'; import { FeaturedService } from '@/core/FeaturedService.js';
const FALLBACK = '❤'; const FALLBACK = '❤';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
const legacies: Record<string, string> = { const legacies: Record<string, string> = {
'like': '👍', 'like': '👍',
@ -187,6 +188,9 @@ export class ReactionService {
await this.notesRepository.createQueryBuilder().update() await this.notesRepository.createQueryBuilder().update()
.set({ .set({
reactions: () => sql, reactions: () => sql,
...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
} : {}),
}) })
.where('id = :id', { id: note.id }) .where('id = :id', { id: note.id })
.execute(); .execute();
@ -293,6 +297,7 @@ export class ReactionService {
await this.notesRepository.createQueryBuilder().update() await this.notesRepository.createQueryBuilder().update()
.set({ .set({
reactions: () => sql, reactions: () => sql,
reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
}) })
.where('id = :id', { id: note.id }) .where('id = :id', { id: note.id })
.execute(); .execute();

View File

@ -170,27 +170,37 @@ export class NoteEntityService implements OnModuleInit {
} }
@bindThis @bindThis
public async populateMyReaction(noteId: MiNote['id'], meId: MiUser['id'], _hint_?: { public async populateMyReaction(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: {
myReactions: Map<MiNote['id'], MiNoteReaction | null>; myReactions: Map<MiNote['id'], string | null>;
}) { }) {
if (_hint_?.myReactions) { if (_hint_?.myReactions) {
const reaction = _hint_.myReactions.get(noteId); const reaction = _hint_.myReactions.get(note.id);
if (reaction) { if (reaction) {
return this.reactionService.convertLegacyReaction(reaction.reaction); return this.reactionService.convertLegacyReaction(reaction);
} else if (reaction === null) { } else {
return undefined;
}
}
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) return undefined;
if (note.reactionAndUserPairCache && reactionsCount <= note.reactionAndUserPairCache.length) {
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
if (pair) {
return this.reactionService.convertLegacyReaction(pair.split('/')[1]);
} else {
return undefined; return undefined;
} }
// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
} }
// パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない // パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない
if (this.idService.parse(noteId).date.getTime() + 2000 > Date.now()) { if (this.idService.parse(note.id).date.getTime() + 2000 > Date.now()) {
return undefined; return undefined;
} }
const reaction = await this.noteReactionsRepository.findOneBy({ const reaction = await this.noteReactionsRepository.findOneBy({
userId: meId, userId: meId,
noteId: noteId, noteId: note.id,
}); });
if (reaction) { if (reaction) {
@ -276,8 +286,9 @@ export class NoteEntityService implements OnModuleInit {
options?: { options?: {
detail?: boolean; detail?: boolean;
skipHide?: boolean; skipHide?: boolean;
withReactionAndUserPairCache?: boolean;
_hint_?: { _hint_?: {
myReactions: Map<MiNote['id'], MiNoteReaction | null>; myReactions: Map<MiNote['id'], string | null>;
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>; packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
}; };
}, },
@ -285,6 +296,7 @@ export class NoteEntityService implements OnModuleInit {
const opts = Object.assign({ const opts = Object.assign({
detail: true, detail: true,
skipHide: false, skipHide: false,
withReactionAndUserPairCache: false,
}, options); }, options);
const meId = me ? me.id : null; const meId = me ? me.id : null;
@ -325,6 +337,7 @@ export class NoteEntityService implements OnModuleInit {
repliesCount: note.repliesCount, repliesCount: note.repliesCount,
reactions: this.reactionService.convertLegacyReactions(note.reactions), reactions: this.reactionService.convertLegacyReactions(note.reactions),
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host), reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined, emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
tags: note.tags.length > 0 ? note.tags : undefined, tags: note.tags.length > 0 ? note.tags : undefined,
fileIds: note.fileIds, fileIds: note.fileIds,
@ -347,18 +360,20 @@ export class NoteEntityService implements OnModuleInit {
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, { reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
detail: false, detail: false,
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
_hint_: options?._hint_, _hint_: options?._hint_,
}) : undefined, }) : undefined,
renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, { renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, {
detail: true, detail: true,
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
_hint_: options?._hint_, _hint_: options?._hint_,
}) : undefined, }) : undefined,
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
...(meId ? { ...(meId && Object.keys(note.reactions).length > 0 ? {
myReaction: this.populateMyReaction(note.id, meId, options?._hint_), myReaction: this.populateMyReaction(note, meId, options?._hint_),
} : {}), } : {}),
} : {}), } : {}),
}); });
@ -382,19 +397,48 @@ export class NoteEntityService implements OnModuleInit {
if (notes.length === 0) return []; if (notes.length === 0) return [];
const meId = me ? me.id : null; const meId = me ? me.id : null;
const myReactionsMap = new Map<MiNote['id'], MiNoteReaction | null>(); const myReactionsMap = new Map<MiNote['id'], string | null>();
if (meId) { if (meId) {
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); const idsNeedFetchMyReaction = new Set<MiNote['id']>();
// パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない // パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない
const oldId = this.idService.gen(Date.now() - 2000); const oldId = this.idService.gen(Date.now() - 2000);
const targets = [...notes.filter(n => n.id < oldId).map(n => n.id), ...renoteIds];
const myReactions = await this.noteReactionsRepository.findBy({
userId: meId,
noteId: In(targets),
});
for (const target of targets) { for (const note of notes) {
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null); if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.renote.id, null);
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) {
const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
} else {
idsNeedFetchMyReaction.add(note.renote.id);
}
} else {
if (note.id < oldId) {
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.id, null);
} else if (reactionsCount <= note.reactionAndUserPairCache.length) {
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
} else {
idsNeedFetchMyReaction.add(note.id);
}
} else {
myReactionsMap.set(note.id, null);
}
}
}
const myReactions = idsNeedFetchMyReaction.size > 0 ? await this.noteReactionsRepository.findBy({
userId: meId,
noteId: In(Array.from(idsNeedFetchMyReaction)),
}) : [];
for (const id of idsNeedFetchMyReaction) {
myReactionsMap.set(id, myReactions.find(reaction => reaction.noteId === id)?.reaction ?? null);
} }
} }

View File

@ -164,6 +164,11 @@ export class MiNote {
}) })
public mentionedRemoteUsers: string; public mentionedRemoteUsers: string;
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public reactionAndUserPairCache: string[];
@Column('varchar', { @Column('varchar', {
length: 128, array: true, default: '{}', length: 128, array: true, default: '{}',
}) })

View File

@ -174,6 +174,14 @@ export const packedNoteSchema = {
type: 'string', type: 'string',
optional: true, nullable: false, optional: true, nullable: false,
}, },
reactionAndUserPairCache: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
myReaction: { myReaction: {
type: 'object', type: 'object',

View File

@ -73,7 +73,7 @@ export class ServerService implements OnApplicationShutdown {
public async launch(): Promise<void> { public async launch(): Promise<void> {
const fastify = Fastify({ const fastify = Fastify({
trustProxy: true, trustProxy: true,
logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''), logger: false,
}); });
this.#fastify = fastify; this.#fastify = fastify;

View File

@ -318,8 +318,9 @@ export class ApiCallService implements OnApplicationShutdown {
} }
if (ep.meta.requireRolePolicy != null && !user!.isRoot) { if (ep.meta.requireRolePolicy != null && !user!.isRoot) {
const myRoles = await this.roleService.getUserRoles(user!.id);
const policies = await this.roleService.getUserPolicies(user!.id); const policies = await this.roleService.getUserPolicies(user!.id);
if (!policies[ep.meta.requireRolePolicy]) { if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
throw new ApiError({ throw new ApiError({
message: 'You are not assigned to a required role.', message: 'You are not assigned to a required role.',
code: 'ROLE_PERMISSION_DENIED', code: 'ROLE_PERMISSION_DENIED',

View File

@ -123,6 +123,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
noteIds.sort((a, b) => a > b ? -1 : 1); noteIds.sort((a, b) => a > b ? -1 : 1);
noteIds = noteIds.slice(0, ps.limit); noteIds = noteIds.slice(0, ps.limit);
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
if (!shouldFallbackToDb) { if (!shouldFallbackToDb) {
const query = this.notesRepository.createQueryBuilder('note') const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds }) .where('note.id IN (:...noteIds)', { noteIds: noteIds })
@ -180,15 +182,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser'); .leftJoinAndSelect('renote.user', 'renoteUser');
query.andWhere(new Brackets(qb => { if (!ps.withReplies) {
qb query.andWhere(new Brackets(qb => {
.where('note.replyId IS NULL') // 返信ではない qb
.orWhere(new Brackets(qb => { .where('note.replyId IS NULL') // 返信ではない
qb // 返信だけど投稿者自身への返信 .orWhere(new Brackets(qb => {
.where('note.replyId IS NOT NULL') qb // 返信だけど投稿者自身への返信
.andWhere('note.replyUserId = note.userId'); .where('note.replyId IS NOT NULL')
})); .andWhere('note.replyUserId = note.userId');
})); }));
}));
}
this.queryService.generateVisibilityQuery(query, me); this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedUserQuery(query, me);

View File

@ -163,6 +163,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.andWhere('note.fileIds != \'{}\''); query.andWhere('note.fileIds != \'{}\'');
} }
if (!ps.withReplies) {
query.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');
}));
}));
}
const timeline = await query.limit(ps.limit).getMany(); const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => { process.nextTick(() => {

View File

@ -46,8 +46,10 @@ class ChannelChannel extends Channel {
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
if (this.user && note.renoteId && !note.text) { if (this.user && note.renoteId && !note.text) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); if (note.renote && Object.keys(note.renote.reactions).length > 0) {
note.renote!.myReaction = myRenoteReaction; const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
} }
this.connection.cacheNote(note); this.connection.cacheNote(note);

View File

@ -72,8 +72,10 @@ class GlobalTimelineChannel extends Channel {
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
if (this.user && note.renoteId && !note.text) { if (this.user && note.renoteId && !note.text) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); if (note.renote && Object.keys(note.renote.reactions).length > 0) {
note.renote!.myReaction = myRenoteReaction; const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
} }
this.connection.cacheNote(note); this.connection.cacheNote(note);

View File

@ -51,8 +51,10 @@ class HashtagChannel extends Channel {
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
if (this.user && note.renoteId && !note.text) { if (this.user && note.renoteId && !note.text) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); if (note.renote && Object.keys(note.renote.reactions).length > 0) {
note.renote!.myReaction = myRenoteReaction; const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
} }
this.connection.cacheNote(note); this.connection.cacheNote(note);

View File

@ -74,8 +74,10 @@ class HomeTimelineChannel extends Channel {
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
if (this.user && note.renoteId && !note.text) { if (this.user && note.renoteId && !note.text) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); if (note.renote && Object.keys(note.renote.reactions).length > 0) {
note.renote!.myReaction = myRenoteReaction; const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
} }
this.connection.cacheNote(note); this.connection.cacheNote(note);

View File

@ -88,8 +88,11 @@ class HybridTimelineChannel extends Channel {
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
if (this.user && note.renoteId && !note.text) { if (this.user && note.renoteId && !note.text) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); if (note.renote && Object.keys(note.renote.reactions).length > 0) {
note.renote!.myReaction = myRenoteReaction; console.log(note.renote.reactionAndUserPairCache);
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
} }
this.connection.cacheNote(note); this.connection.cacheNote(note);

View File

@ -71,8 +71,10 @@ class LocalTimelineChannel extends Channel {
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
if (this.user && note.renoteId && !note.text) { if (this.user && note.renoteId && !note.text) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); if (note.renote && Object.keys(note.renote.reactions).length > 0) {
note.renote!.myReaction = myRenoteReaction; const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
} }
this.connection.cacheNote(note); this.connection.cacheNote(note);

View File

@ -103,8 +103,10 @@ class UserListChannel extends Channel {
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
if (this.user && note.renoteId && !note.text) { if (this.user && note.renoteId && !note.text) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); if (note.renote && Object.keys(note.renote.reactions).length > 0) {
note.renote!.myReaction = myRenoteReaction; const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
} }
this.connection.cacheNote(note); this.connection.cacheNote(note);

View File

@ -29,7 +29,7 @@
"@vue/compiler-sfc": "3.3.4", "@vue/compiler-sfc": "3.3.4",
"astring": "1.8.6", "astring": "1.8.6",
"autosize": "6.0.1", "autosize": "6.0.1",
"broadcast-channel": "5.4.0", "broadcast-channel": "5.5.0",
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"buraha": "0.0.1", "buraha": "0.0.1",
"canvas-confetti": "1.6.1", "canvas-confetti": "1.6.1",
@ -59,7 +59,7 @@
"querystring": "0.2.1", "querystring": "0.2.1",
"rollup": "4.1.4", "rollup": "4.1.4",
"sanitize-html": "2.11.0", "sanitize-html": "2.11.0",
"sass": "1.69.3", "sass": "1.69.4",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.157.0", "three": "0.157.0",
@ -72,7 +72,7 @@
"uuid": "9.0.1", "uuid": "9.0.1",
"v-code-diff": "1.7.1", "v-code-diff": "1.7.1",
"vanilla-tilt": "1.8.1", "vanilla-tilt": "1.8.1",
"vite": "4.4.11", "vite": "4.5.0",
"vue": "3.3.4", "vue": "3.3.4",
"vue-prism-editor": "2.0.0-alpha.2", "vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "next" "vuedraggable": "next"
@ -97,25 +97,25 @@
"@storybook/vue3": "7.5.0", "@storybook/vue3": "7.5.0",
"@storybook/vue3-vite": "7.5.0", "@storybook/vue3-vite": "7.5.0",
"@testing-library/vue": "7.0.0", "@testing-library/vue": "7.0.0",
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.2",
"@types/estree": "1.0.2", "@types/estree": "1.0.3",
"@types/matter-js": "0.19.1", "@types/matter-js": "0.19.2",
"@types/micromatch": "4.0.3", "@types/micromatch": "4.0.4",
"@types/node": "20.8.6", "@types/node": "20.8.7",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.1",
"@types/sanitize-html": "2.9.2", "@types/sanitize-html": "2.9.3",
"@types/throttle-debounce": "5.0.0", "@types/throttle-debounce": "5.0.1",
"@types/tinycolor2": "1.4.4", "@types/tinycolor2": "1.4.5",
"@types/uuid": "9.0.5", "@types/uuid": "9.0.6",
"@types/websocket": "1.0.7", "@types/websocket": "1.0.8",
"@types/ws": "8.5.7", "@types/ws": "8.5.8",
"@typescript-eslint/eslint-plugin": "6.8.0", "@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0", "@typescript-eslint/parser": "6.8.0",
"@vitest/coverage-v8": "0.34.6", "@vitest/coverage-v8": "0.34.6",
"@vue/runtime-core": "3.3.4", "@vue/runtime-core": "3.3.4",
"acorn": "8.10.0", "acorn": "8.10.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.3.1", "cypress": "13.3.2",
"eslint": "8.51.0", "eslint": "8.51.0",
"eslint-plugin-import": "2.28.1", "eslint-plugin-import": "2.28.1",
"eslint-plugin-vue": "9.17.0", "eslint-plugin-vue": "9.17.0",

View File

@ -232,6 +232,7 @@ const keymap = {
useNoteCapture({ useNoteCapture({
rootEl: el, rootEl: el,
note: $$(appearNote), note: $$(appearNote),
pureNote: $$(note),
isDeletedRef: isDeleted, isDeletedRef: isDeleted,
}); });

View File

@ -296,6 +296,7 @@ const reactionsPagination = $computed(() => ({
useNoteCapture({ useNoteCapture({
rootEl: el, rootEl: el,
note: $$(appearNote), note: $$(appearNote),
pureNote: $$(note),
isDeletedRef: isDeleted, isDeletedRef: isDeleted,
}); });

View File

@ -17,6 +17,7 @@ import MkSparkle from '@/components/MkSparkle.vue';
import MkA from '@/components/global/MkA.vue'; import MkA from '@/components/global/MkA.vue';
import { host } from '@/config.js'; import { host } from '@/config.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { nyaize } from '@/scripts/nyaize.js';
const QUOTE_STYLE = ` const QUOTE_STYLE = `
display: block; display: block;
@ -55,10 +56,13 @@ export default function(props: {
* @param ast MFM AST * @param ast MFM AST
* @param scale How times large the text is * @param scale How times large the text is
*/ */
const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => { const genEl = (ast: mfm.MfmNode[], scale: number, disableNyaize = false) => ast.map((token): VNode | string | (VNode | string)[] => {
switch (token.type) { switch (token.type) {
case 'text': { case 'text': {
const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
if (!disableNyaize && props.author?.isCat) {
text = nyaize(text);
}
if (!props.plain) { if (!props.plain) {
const res: (VNode | string)[] = []; const res: (VNode | string)[] = [];
@ -260,7 +264,7 @@ export default function(props: {
key: Math.random(), key: Math.random(),
url: token.props.url, url: token.props.url,
rel: 'nofollow noopener', rel: 'nofollow noopener',
}, genEl(token.children, scale))]; }, genEl(token.children, scale, true))];
} }
case 'mention': { case 'mention': {
@ -299,11 +303,11 @@ export default function(props: {
if (!props.nowrap) { if (!props.nowrap) {
return [h('div', { return [h('div', {
style: QUOTE_STYLE, style: QUOTE_STYLE,
}, genEl(token.children, scale))]; }, genEl(token.children, scale, true))];
} else { } else {
return [h('span', { return [h('span', {
style: QUOTE_STYLE, style: QUOTE_STYLE,
}, genEl(token.children, scale))]; }, genEl(token.children, scale, true))];
} }
} }
@ -358,7 +362,7 @@ export default function(props: {
} }
case 'plain': { case 'plain': {
return [h('span', genEl(token.children, scale))]; return [h('span', genEl(token.children, scale, true))];
} }
default: { default: {

View File

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function nyaize(text: string): string {
return text
// ja-JP
.replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
// en-US
.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
// ko-KR
.replace(/[나-낳]/g, match => String.fromCharCode(
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
))
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');
}

View File

@ -11,15 +11,17 @@ import { $i } from '@/account.js';
export function useNoteCapture(props: { export function useNoteCapture(props: {
rootEl: Ref<HTMLElement>; rootEl: Ref<HTMLElement>;
note: Ref<Misskey.entities.Note>; note: Ref<Misskey.entities.Note>;
pureNote: Ref<Misskey.entities.Note>;
isDeletedRef: Ref<boolean>; isDeletedRef: Ref<boolean>;
}) { }) {
const note = props.note; const note = props.note;
const pureNote = props.pureNote;
const connection = $i ? useStream() : null; const connection = $i ? useStream() : null;
function onStreamNoteUpdated(noteData): void { function onStreamNoteUpdated(noteData): void {
const { type, id, body } = noteData; const { type, id, body } = noteData;
if (id !== note.value.id) return; if ((id !== note.value.id) && (id !== pureNote.value.id)) return;
switch (type) { switch (type) {
case 'reacted': { case 'reacted': {
@ -82,6 +84,7 @@ export function useNoteCapture(props: {
if (connection) { if (connection) {
// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: note.value.id }); connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: note.value.id });
if (pureNote.value.id !== note.value.id) connection.send('s', { id: pureNote.value.id });
if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated); if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
} }
} }
@ -91,6 +94,11 @@ export function useNoteCapture(props: {
connection.send('un', { connection.send('un', {
id: note.value.id, id: note.value.id,
}); });
if (pureNote.value.id !== note.value.id) {
connection.send('un', {
id: pureNote.value.id,
});
}
if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated); if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated);
} }
} }

View File

@ -2977,7 +2977,7 @@ type UserLite = {
id: ID; id: ID;
username: string; username: string;
host: string | null; host: string | null;
name: string; name: string | null;
onlineStatus: 'online' | 'active' | 'offline' | 'unknown'; onlineStatus: 'online' | 'active' | 'offline' | 'unknown';
avatarUrl: string; avatarUrl: string;
avatarBlurhash: string; avatarBlurhash: string;
@ -2993,6 +2993,8 @@ type UserLite = {
faviconUrl: Instance['faviconUrl']; faviconUrl: Instance['faviconUrl'];
themeColor: Instance['themeColor']; themeColor: Instance['themeColor'];
}; };
isCat?: boolean;
isBot?: boolean;
}; };
// @public (undocumented) // @public (undocumented)
@ -3003,8 +3005,8 @@ 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: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:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
// src/api.types.ts:633:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/api.types.ts:633: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:109:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
// src/entities.ts:603:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/entities.ts:605: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 // 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) // (No @packageDocumentation comment for this package)

View File

@ -22,8 +22,8 @@
"devDependencies": { "devDependencies": {
"@microsoft/api-extractor": "7.38.0", "@microsoft/api-extractor": "7.38.0",
"@swc/jest": "0.2.29", "@swc/jest": "0.2.29",
"@types/jest": "29.5.5", "@types/jest": "29.5.6",
"@types/node": "20.8.6", "@types/node": "20.8.7",
"@typescript-eslint/eslint-plugin": "6.8.0", "@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0", "@typescript-eslint/parser": "6.8.0",
"eslint": "8.51.0", "eslint": "8.51.0",

View File

@ -12,7 +12,7 @@ export type UserLite = {
id: ID; id: ID;
username: string; username: string;
host: string | null; host: string | null;
name: string; name: string | null;
onlineStatus: 'online' | 'active' | 'offline' | 'unknown'; onlineStatus: 'online' | 'active' | 'offline' | 'unknown';
avatarUrl: string; avatarUrl: string;
avatarBlurhash: string; avatarBlurhash: string;
@ -28,6 +28,8 @@ export type UserLite = {
faviconUrl: Instance['faviconUrl']; faviconUrl: Instance['faviconUrl'];
themeColor: Instance['themeColor']; themeColor: Instance['themeColor'];
}; };
isCat?: boolean;
isBot?: boolean;
}; };
export type UserDetailed = UserLite & { export type UserDetailed = UserLite & {

View File

@ -9,7 +9,7 @@
"lint": "pnpm typecheck && pnpm eslint" "lint": "pnpm typecheck && pnpm eslint"
}, },
"dependencies": { "dependencies": {
"esbuild": "0.19.4", "esbuild": "0.19.5",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"misskey-js": "workspace:*" "misskey-js": "workspace:*"
}, },

File diff suppressed because it is too large Load Diff