Merge remote-tracking branch 'mi-dev/develop' into emoji-request
# Conflicts: # package.json
This commit is contained in:
commit
f5658cd47d
.devcontainer
.github/workflows
api-misskey-js.ymlcheck_copyright_year.ymldocker-develop.ymldocker.ymldockle.ymllint.ymlpr-preview-deploy.ymltest-backend.ymltest-frontend.ymltest-misskey-js.ymltest-production.yml
CHANGELOG.mdcypress/e2e
locales
de-DE.ymlen-US.ymles-ES.ymlfr-FR.ymlindex.d.tsit-IT.ymlja-JP.ymlja-KS.ymlko-KR.ymlth-TH.ymlzh-TW.yml
package.jsonpackages
backend
migration
1697247230117-InstanceSilence.js1697441463087-FollowRequestWithReplies.js1697673894459-note-reactionAndUserPairCache.js
package.jsonsrc
core
AntennaService.tsCoreModule.tsCustomEmojiService.tsFunoutTimelineService.tsHashtagService.tsNoteCreateService.tsQueueService.tsReactionService.tsRoleService.tsUserFollowingService.tsUtilityService.ts
activitypub
entities
daemons
models
queue
processors
types.tsserver
ServerService.ts
api
web
test/e2e
frontend
package.json
src
components
pages
scripts
store.ts
|
@ -5,7 +5,7 @@
|
||||||
"workspaceFolder": "/workspace",
|
"workspaceFolder": "/workspace",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers-contrib/features/pnpm:2": {
|
"ghcr.io/devcontainers-contrib/features/pnpm:2": {
|
||||||
"version": "8.8.0"
|
"version": "8.9.2"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"version": "20.5.1"
|
"version": "20.5.1"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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!"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -16,12 +16,24 @@
|
||||||
|
|
||||||
### General
|
### General
|
||||||
- Feat: アンテナでローカルの投稿のみ収集できるようになりました
|
- Feat: アンテナでローカルの投稿のみ収集できるようになりました
|
||||||
|
- Feat: サーバーサイレンス機能が追加されました
|
||||||
|
- Enhance: 新規にフォローした人の返信をデフォルトでTLに追加できるオプションを追加
|
||||||
|
- Enhance: HTL/LTL/STLを2023.10.0アップデート以前まで遡れるように
|
||||||
|
- Enhance: フォロー/フォロー解除したときに過去分のHTLにも含まれる投稿が反映されるように
|
||||||
|
- Enhance: ローカリゼーションの更新
|
||||||
|
- Enhance: 依存関係の更新
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Enhance: TLの返信表示オプションを記憶するように
|
- Enhance: TLの返信表示オプションを記憶するように
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
- Enhance: タイムライン取得時のパフォーマンスを向上
|
||||||
- Enhance: ストリーミングAPIのパフォーマンスを向上
|
- Enhance: ストリーミングAPIのパフォーマンスを向上
|
||||||
|
- Fix: users/notesでDBから参照した際にチャンネル投稿のみ取得される問題を修正
|
||||||
|
- Fix: コントロールパネルの設定項目が正しく保存できない問題を修正
|
||||||
|
- Fix: 管理者権限のロールを持っていても一部のAPIが使用できないことがある問題を修正
|
||||||
|
- Change: ユーザーのisCatがtrueでも、サーバーではnyaizeが行われなくなりました
|
||||||
|
- isCatな場合、クライアントでnyaize処理を行うことを推奨します
|
||||||
|
|
||||||
## 2023.10.1
|
## 2023.10.1
|
||||||
### General
|
### General
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
|
@ -195,6 +195,7 @@ perHour: "Pro Stunde"
|
||||||
perDay: "Pro Tag"
|
perDay: "Pro Tag"
|
||||||
stopActivityDelivery: "Senden von Aktivitäten einstellen"
|
stopActivityDelivery: "Senden von Aktivitäten einstellen"
|
||||||
blockThisInstance: "Diese Instanz blockieren"
|
blockThisInstance: "Diese Instanz blockieren"
|
||||||
|
silenceThisInstance: "Instanz stummschalten"
|
||||||
operations: "Aktionen"
|
operations: "Aktionen"
|
||||||
software: "Software"
|
software: "Software"
|
||||||
version: "Version"
|
version: "Version"
|
||||||
|
@ -214,6 +215,8 @@ clearCachedFiles: "Cache leeren"
|
||||||
clearCachedFilesConfirm: "Sollen alle im Cache gespeicherten Dateien von anderen Instanzen wirklich gelöscht werden?"
|
clearCachedFilesConfirm: "Sollen alle im Cache gespeicherten Dateien von anderen Instanzen wirklich gelöscht werden?"
|
||||||
blockedInstances: "Blockierte Instanzen"
|
blockedInstances: "Blockierte Instanzen"
|
||||||
blockedInstancesDescription: "Gib die Hostnamen der Instanzen, welche blockiert werden sollen, durch Zeilenumbrüche getrennt an. Blockierte Instanzen können mit dieser instanz nicht mehr kommunizieren."
|
blockedInstancesDescription: "Gib die Hostnamen der Instanzen, welche blockiert werden sollen, durch Zeilenumbrüche getrennt an. Blockierte Instanzen können mit dieser instanz nicht mehr kommunizieren."
|
||||||
|
silencedInstances: "Stummgeschaltete Instanzen"
|
||||||
|
silencedInstancesDescription: "Gib die Hostnamen der Instanzen, welche stummgeschaltet werden sollen, durch Zeilenumbrüche getrennt an. Alle Konten dieser Instanzen werden als stummgeschaltet behandelt, können nur noch Follow-Anfragen stellen und wenn nicht gefolgt keine lokalen Konten erwähnen. Blockierte Instanzen sind davon nicht betroffen."
|
||||||
muteAndBlock: "Stummschaltungen und Blockierungen"
|
muteAndBlock: "Stummschaltungen und Blockierungen"
|
||||||
mutedUsers: "Stummgeschaltete Benutzer"
|
mutedUsers: "Stummgeschaltete Benutzer"
|
||||||
blockedUsers: "Blockierte Benutzer"
|
blockedUsers: "Blockierte Benutzer"
|
||||||
|
@ -531,6 +534,7 @@ serverLogs: "Serverprotokolle"
|
||||||
deleteAll: "Alle löschen"
|
deleteAll: "Alle löschen"
|
||||||
showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen"
|
showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen"
|
||||||
showFixedPostFormInChannel: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen (Kanäle)"
|
showFixedPostFormInChannel: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen (Kanäle)"
|
||||||
|
withRepliesByDefaultForNewlyFollowed: "Standardmäßig Antworten von neu gefolgten Benutzern in der Chronik anzeigen"
|
||||||
newNoteRecived: "Es gibt neue Notizen"
|
newNoteRecived: "Es gibt neue Notizen"
|
||||||
sounds: "Töne"
|
sounds: "Töne"
|
||||||
sound: "Töne"
|
sound: "Töne"
|
||||||
|
@ -794,7 +798,7 @@ active: "Aktiv"
|
||||||
offline: "Offline"
|
offline: "Offline"
|
||||||
notRecommended: "Nicht empfohlen"
|
notRecommended: "Nicht empfohlen"
|
||||||
botProtection: "Schutz vor Bots"
|
botProtection: "Schutz vor Bots"
|
||||||
instanceBlocking: "Blockierte Instanzen"
|
instanceBlocking: "Blockierte/Stummgeschaltete Instanzen"
|
||||||
selectAccount: "Benutzerkonto auswählen"
|
selectAccount: "Benutzerkonto auswählen"
|
||||||
switchAccount: "Konto wechseln"
|
switchAccount: "Konto wechseln"
|
||||||
enabled: "Aktiviert"
|
enabled: "Aktiviert"
|
||||||
|
@ -1921,6 +1925,7 @@ _exportOrImport:
|
||||||
userLists: "Listen"
|
userLists: "Listen"
|
||||||
excludeMutingUsers: "Stummgeschaltete Benutzer aussortieren"
|
excludeMutingUsers: "Stummgeschaltete Benutzer aussortieren"
|
||||||
excludeInactiveUsers: "Inaktive Benutzer aussortieren"
|
excludeInactiveUsers: "Inaktive Benutzer aussortieren"
|
||||||
|
withReplies: "Antworten von importierten Benutzern in der Chronik beinhalten"
|
||||||
_charts:
|
_charts:
|
||||||
federation: "Föderation"
|
federation: "Föderation"
|
||||||
apRequest: "Anfragen"
|
apRequest: "Anfragen"
|
||||||
|
|
|
@ -195,6 +195,7 @@ perHour: "Per Hour"
|
||||||
perDay: "Per Day"
|
perDay: "Per Day"
|
||||||
stopActivityDelivery: "Stop sending activities"
|
stopActivityDelivery: "Stop sending activities"
|
||||||
blockThisInstance: "Block this instance"
|
blockThisInstance: "Block this instance"
|
||||||
|
silenceThisInstance: "Silence this instance"
|
||||||
operations: "Operations"
|
operations: "Operations"
|
||||||
software: "Software"
|
software: "Software"
|
||||||
version: "Version"
|
version: "Version"
|
||||||
|
@ -213,7 +214,9 @@ clearQueueConfirmText: "Any undelivered notes remaining in the queue will not be
|
||||||
clearCachedFiles: "Clear cache"
|
clearCachedFiles: "Clear cache"
|
||||||
clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote files?"
|
clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote files?"
|
||||||
blockedInstances: "Blocked Instances"
|
blockedInstances: "Blocked Instances"
|
||||||
blockedInstancesDescription: "List the hostnames of the instances that you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance."
|
blockedInstancesDescription: "List the hostnames of the instances you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance."
|
||||||
|
silencedInstances: "Silenced instances"
|
||||||
|
silencedInstancesDescription: "List the hostnames of the instances that you want to silence. All accounts of the listed instances will be treated as silenced, can only make follow requests, and cannot mention local accounts if not followed. This will not affect blocked instances."
|
||||||
muteAndBlock: "Mutes and Blocks"
|
muteAndBlock: "Mutes and Blocks"
|
||||||
mutedUsers: "Muted users"
|
mutedUsers: "Muted users"
|
||||||
blockedUsers: "Blocked users"
|
blockedUsers: "Blocked users"
|
||||||
|
@ -531,6 +534,7 @@ serverLogs: "Server logs"
|
||||||
deleteAll: "Delete all"
|
deleteAll: "Delete all"
|
||||||
showFixedPostForm: "Display the posting form at the top of the timeline"
|
showFixedPostForm: "Display the posting form at the top of the timeline"
|
||||||
showFixedPostFormInChannel: "Display the posting form at the top of the timeline (Channels)"
|
showFixedPostFormInChannel: "Display the posting form at the top of the timeline (Channels)"
|
||||||
|
withRepliesByDefaultForNewlyFollowed: "Include replies by newly followed users in the timeline by default"
|
||||||
newNoteRecived: "There are new notes"
|
newNoteRecived: "There are new notes"
|
||||||
sounds: "Sounds"
|
sounds: "Sounds"
|
||||||
sound: "Sounds"
|
sound: "Sounds"
|
||||||
|
@ -589,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"
|
||||||
|
@ -794,7 +798,7 @@ active: "Active"
|
||||||
offline: "Offline"
|
offline: "Offline"
|
||||||
notRecommended: "Not recommended"
|
notRecommended: "Not recommended"
|
||||||
botProtection: "Bot Protection"
|
botProtection: "Bot Protection"
|
||||||
instanceBlocking: "Blocked Instances"
|
instanceBlocking: "Blocked/Silenced Instances"
|
||||||
selectAccount: "Select account"
|
selectAccount: "Select account"
|
||||||
switchAccount: "Switch account"
|
switchAccount: "Switch account"
|
||||||
enabled: "Enabled"
|
enabled: "Enabled"
|
||||||
|
@ -1126,8 +1130,8 @@ edited: "Edited"
|
||||||
notificationRecieveConfig: "Notification Settings"
|
notificationRecieveConfig: "Notification Settings"
|
||||||
mutualFollow: "Mutual follow"
|
mutualFollow: "Mutual follow"
|
||||||
fileAttachedOnly: "Only notes with files"
|
fileAttachedOnly: "Only notes with files"
|
||||||
showRepliesToOthersInTimeline: "Show replies to others in TL"
|
showRepliesToOthersInTimeline: "Show replies to others in timeline"
|
||||||
hideRepliesToOthersInTimeline: "Hide replies to others from TL"
|
hideRepliesToOthersInTimeline: "Hide replies to others from timeline"
|
||||||
externalServices: "External Services"
|
externalServices: "External Services"
|
||||||
impressum: "Impressum"
|
impressum: "Impressum"
|
||||||
impressumUrl: "Impressum URL"
|
impressumUrl: "Impressum URL"
|
||||||
|
@ -1738,7 +1742,7 @@ _2fa:
|
||||||
step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app."
|
step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app."
|
||||||
step2Uri: "Enter the following URI if you are using a desktop program"
|
step2Uri: "Enter the following URI if you are using a desktop program"
|
||||||
step3Title: "Enter an authentication code"
|
step3Title: "Enter an authentication code"
|
||||||
step3: "Enter the token provided by your app to finish setup."
|
step3: "Enter the authentication code (token) provided by your app to finish setup."
|
||||||
setupCompleted: "Setup complete"
|
setupCompleted: "Setup complete"
|
||||||
step4: "From now on, any future login attempts will ask for such a login token."
|
step4: "From now on, any future login attempts will ask for such a login token."
|
||||||
securityKeyNotSupported: "Your browser does not support security keys."
|
securityKeyNotSupported: "Your browser does not support security keys."
|
||||||
|
@ -1921,6 +1925,7 @@ _exportOrImport:
|
||||||
userLists: "User lists"
|
userLists: "User lists"
|
||||||
excludeMutingUsers: "Exclude muted users"
|
excludeMutingUsers: "Exclude muted users"
|
||||||
excludeInactiveUsers: "Exclude inactive users"
|
excludeInactiveUsers: "Exclude inactive users"
|
||||||
|
withReplies: "Include replies from imported users in the timeline"
|
||||||
_charts:
|
_charts:
|
||||||
federation: "Federation"
|
federation: "Federation"
|
||||||
apRequest: "Requests"
|
apRequest: "Requests"
|
||||||
|
@ -2073,7 +2078,7 @@ _deck:
|
||||||
introduction: "Create the perfect interface for you by arranging columns freely!"
|
introduction: "Create the perfect interface for you by arranging columns freely!"
|
||||||
introduction2: "Click on the + on the right of the screen to add new colums whenever you want."
|
introduction2: "Click on the + on the right of the screen to add new colums whenever you want."
|
||||||
widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget."
|
widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget."
|
||||||
useSimpleUiForNonRootPages: "Use simplified UI to navigated pages"
|
useSimpleUiForNonRootPages: "Use simple UI for navigated pages"
|
||||||
usedAsMinWidthWhenFlexible: "Minimum width will be used for this when the \"Auto-adjust width\" option is enabled"
|
usedAsMinWidthWhenFlexible: "Minimum width will be used for this when the \"Auto-adjust width\" option is enabled"
|
||||||
flexible: "Auto-adjust width"
|
flexible: "Auto-adjust width"
|
||||||
_columns:
|
_columns:
|
||||||
|
|
|
@ -195,6 +195,7 @@ perHour: "por hora"
|
||||||
perDay: "por día"
|
perDay: "por día"
|
||||||
stopActivityDelivery: "Dejar de enviar actividades"
|
stopActivityDelivery: "Dejar de enviar actividades"
|
||||||
blockThisInstance: "Bloquear instancia"
|
blockThisInstance: "Bloquear instancia"
|
||||||
|
silenceThisInstance: "Silenciar esta instancia"
|
||||||
operations: "Operaciones"
|
operations: "Operaciones"
|
||||||
software: "Software"
|
software: "Software"
|
||||||
version: "Versión"
|
version: "Versión"
|
||||||
|
@ -214,6 +215,8 @@ clearCachedFiles: "Limpiar caché"
|
||||||
clearCachedFilesConfirm: "¿Desea borrar todos los archivos remotos cacheados?"
|
clearCachedFilesConfirm: "¿Desea borrar todos los archivos remotos cacheados?"
|
||||||
blockedInstances: "Instancias bloqueadas"
|
blockedInstances: "Instancias bloqueadas"
|
||||||
blockedInstancesDescription: "Seleccione los hosts de las instancias que desea bloquear, separadas por una linea nueva. Las instancias bloqueadas no podrán comunicarse con esta instancia."
|
blockedInstancesDescription: "Seleccione los hosts de las instancias que desea bloquear, separadas por una linea nueva. Las instancias bloqueadas no podrán comunicarse con esta instancia."
|
||||||
|
silencedInstances: "Instancias silenciadas"
|
||||||
|
silencedInstancesDescription: "Listar los hostname de las instancias que quieres silenciar. Todas las cuentas de las instancias listadas serán tratadas como silenciadas, solo podrán hacer peticiones de seguimiento, y no podrán mencionar cuentas locales si no las siguen. Esto no afecta a las instancias bloqueadas."
|
||||||
muteAndBlock: "Silenciar y bloquear"
|
muteAndBlock: "Silenciar y bloquear"
|
||||||
mutedUsers: "Usuarios silenciados"
|
mutedUsers: "Usuarios silenciados"
|
||||||
blockedUsers: "Usuarios bloqueados"
|
blockedUsers: "Usuarios bloqueados"
|
||||||
|
@ -531,6 +534,7 @@ serverLogs: "Registros del servidor"
|
||||||
deleteAll: "Eliminar todos"
|
deleteAll: "Eliminar todos"
|
||||||
showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo"
|
showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo"
|
||||||
showFixedPostFormInChannel: "Mostrar el formulario de publicación por encima de la cronología (Canales)"
|
showFixedPostFormInChannel: "Mostrar el formulario de publicación por encima de la cronología (Canales)"
|
||||||
|
withRepliesByDefaultForNewlyFollowed: "Incluir por defecto respuestas de usuarios recién seguidos en la línea de tiempo"
|
||||||
newNoteRecived: "Tienes una nota nueva"
|
newNoteRecived: "Tienes una nota nueva"
|
||||||
sounds: "Sonidos"
|
sounds: "Sonidos"
|
||||||
sound: "Sonidos"
|
sound: "Sonidos"
|
||||||
|
@ -1121,6 +1125,20 @@ unnotifyNotes: "Dejar de notificar nuevas notas"
|
||||||
authentication: "Autenticación"
|
authentication: "Autenticación"
|
||||||
authenticationRequiredToContinue: "Por favor, autentifícate para continuar"
|
authenticationRequiredToContinue: "Por favor, autentifícate para continuar"
|
||||||
dateAndTime: "Fecha y hora"
|
dateAndTime: "Fecha y hora"
|
||||||
|
showRenotes: "Mostrar renotas"
|
||||||
|
edited: "Editado"
|
||||||
|
notificationRecieveConfig: "Ajustes de Notificaciones"
|
||||||
|
mutualFollow: "Os seguís mutuamente"
|
||||||
|
fileAttachedOnly: "Solo notas con archivos"
|
||||||
|
showRepliesToOthersInTimeline: "Mostrar respuestas a otros en la línea de tiempo"
|
||||||
|
hideRepliesToOthersInTimeline: "Ocultar respuestas a otros en la línea de tiempo"
|
||||||
|
externalServices: "Servicios Externos"
|
||||||
|
impressum: "Impressum"
|
||||||
|
impressumUrl: "Impressum URL"
|
||||||
|
impressumDescription: "En algunos países, como Alemania, la inclusión del operador de datos (el Impressum) es requerido legalmente para sitios web comerciales."
|
||||||
|
privacyPolicy: "Política de Privacidad"
|
||||||
|
privacyPolicyUrl: "URL de la Política de Privacidad"
|
||||||
|
tosAndPrivacyPolicy: "Condiciones de Uso y Política de Privacidad"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "Solo para usuarios registrados"
|
forExistingUsers: "Solo para usuarios registrados"
|
||||||
forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán."
|
forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán."
|
||||||
|
@ -1470,6 +1488,7 @@ _role:
|
||||||
descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos"
|
descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos"
|
||||||
canHideAds: "Puede ocultar anuncios"
|
canHideAds: "Puede ocultar anuncios"
|
||||||
canSearchNotes: "Uso de la búsqueda de notas"
|
canSearchNotes: "Uso de la búsqueda de notas"
|
||||||
|
canUseTranslator: "Uso de traductor"
|
||||||
_condition:
|
_condition:
|
||||||
isLocal: "Usuario local"
|
isLocal: "Usuario local"
|
||||||
isRemote: "Usuario remoto"
|
isRemote: "Usuario remoto"
|
||||||
|
@ -1518,6 +1537,10 @@ _ad:
|
||||||
reduceFrequencyOfThisAd: "Mostrar menos este anuncio."
|
reduceFrequencyOfThisAd: "Mostrar menos este anuncio."
|
||||||
hide: "No mostrar"
|
hide: "No mostrar"
|
||||||
timezoneinfo: "El día de la semana está determidado por la zona horaria del servidor."
|
timezoneinfo: "El día de la semana está determidado por la zona horaria del servidor."
|
||||||
|
adsSettings: "Ajustes de anuncios"
|
||||||
|
notesPerOneAd: "Intervalo de actualización de anuncios en tiempo real (Notas por cada anuncio)"
|
||||||
|
setZeroToDisable: "Establece este valor a 0 para deshabilitar la actualización de anuncios en tiempo real"
|
||||||
|
adsTooClose: "El intervalo de anuncios actual puede empeorar la experiencia del usuario por ser demasiado bajo."
|
||||||
_forgotPassword:
|
_forgotPassword:
|
||||||
enterEmail: "Ingrese el correo usado para registrar la cuenta. Se enviará un link para resetear la contraseña."
|
enterEmail: "Ingrese el correo usado para registrar la cuenta. Se enviará un link para resetear la contraseña."
|
||||||
ifNoEmail: "Si no utilizó un correo para crear la cuenta, contáctese con el administrador."
|
ifNoEmail: "Si no utilizó un correo para crear la cuenta, contáctese con el administrador."
|
||||||
|
@ -1902,6 +1925,7 @@ _exportOrImport:
|
||||||
userLists: "Listas"
|
userLists: "Listas"
|
||||||
excludeMutingUsers: "Excluir usuarios silenciados"
|
excludeMutingUsers: "Excluir usuarios silenciados"
|
||||||
excludeInactiveUsers: "Excluir usuarios inactivos"
|
excludeInactiveUsers: "Excluir usuarios inactivos"
|
||||||
|
withReplies: "Incluir respuestas de los usuarios importados en la línea de tiempo"
|
||||||
_charts:
|
_charts:
|
||||||
federation: "Federación"
|
federation: "Federación"
|
||||||
apRequest: "Pedidos"
|
apRequest: "Pedidos"
|
||||||
|
@ -2119,3 +2143,14 @@ _moderationLogTypes:
|
||||||
unmarkSensitiveDriveFile: "Archivo marcado como no sensible"
|
unmarkSensitiveDriveFile: "Archivo marcado como no sensible"
|
||||||
resolveAbuseReport: "Reporte resuelto"
|
resolveAbuseReport: "Reporte resuelto"
|
||||||
createInvitation: "Generar invitación"
|
createInvitation: "Generar invitación"
|
||||||
|
createAd: "Anuncio creado"
|
||||||
|
deleteAd: "Anuncio eliminado"
|
||||||
|
updateAd: "Anuncio actualizado"
|
||||||
|
_fileViewer:
|
||||||
|
title: "Detalles del archivo"
|
||||||
|
type: "Tipo de archivo"
|
||||||
|
size: "Tamaño del archivo"
|
||||||
|
url: "URL"
|
||||||
|
uploadedAt: "Subido el"
|
||||||
|
attachedNotes: "Notas adjuntas"
|
||||||
|
thisPageCanBeSeenFromTheAuthor: "Esta página solo puede ser vista por el autor."
|
||||||
|
|
|
@ -184,7 +184,7 @@ selectUser: "Sélectionner un·e utilisateur·rice"
|
||||||
recipient: "Destinataire"
|
recipient: "Destinataire"
|
||||||
annotation: "Commentaires"
|
annotation: "Commentaires"
|
||||||
federation: "Fédération"
|
federation: "Fédération"
|
||||||
instances: "Instance"
|
instances: "Instances"
|
||||||
registeredAt: "Premier contact le"
|
registeredAt: "Premier contact le"
|
||||||
latestRequestReceivedAt: "Dernière requête reçue"
|
latestRequestReceivedAt: "Dernière requête reçue"
|
||||||
latestStatus: "Dernier statut"
|
latestStatus: "Dernier statut"
|
||||||
|
@ -194,6 +194,7 @@ perHour: "par heure"
|
||||||
perDay: "par jour"
|
perDay: "par jour"
|
||||||
stopActivityDelivery: "Arrêter l’envoi de l’activité"
|
stopActivityDelivery: "Arrêter l’envoi de l’activité"
|
||||||
blockThisInstance: "Bloquer cette instance"
|
blockThisInstance: "Bloquer cette instance"
|
||||||
|
silenceThisInstance: "Mettre cette instance en sourdine"
|
||||||
operations: "Opérations"
|
operations: "Opérations"
|
||||||
software: "Logiciel"
|
software: "Logiciel"
|
||||||
version: "Version"
|
version: "Version"
|
||||||
|
@ -213,6 +214,8 @@ clearCachedFiles: "Vider le cache"
|
||||||
clearCachedFilesConfirm: "Êtes-vous sûr·e de vouloir vider tout le cache de fichiers distants ?"
|
clearCachedFilesConfirm: "Êtes-vous sûr·e de vouloir vider tout le cache de fichiers distants ?"
|
||||||
blockedInstances: "Instances bloquées"
|
blockedInstances: "Instances bloquées"
|
||||||
blockedInstancesDescription: "Listez les instances que vous désirez bloquer, une par ligne. Ces instances ne seront plus en capacité d'interagir avec votre instance."
|
blockedInstancesDescription: "Listez les instances que vous désirez bloquer, une par ligne. Ces instances ne seront plus en capacité d'interagir avec votre instance."
|
||||||
|
silencedInstances: "Instances mises en sourdine"
|
||||||
|
silencedInstancesDescription: "Énumérer les noms d'hôte des instances à mettre en sourdine. Tous les comptes des instances énumérées seront traités comme mis en sourdine, ne peuvent faire que des demandes de suivi et ne peuvent pas mentionner les comptes locaux s'ils ne sont pas suivis. Cela n'affectera pas les instances bloquées."
|
||||||
muteAndBlock: "Masqué·e·s / Bloqué·e·s"
|
muteAndBlock: "Masqué·e·s / Bloqué·e·s"
|
||||||
mutedUsers: "Utilisateur·rice·s en sourdine"
|
mutedUsers: "Utilisateur·rice·s en sourdine"
|
||||||
blockedUsers: "Utilisateur·rice·s bloqué·e·s"
|
blockedUsers: "Utilisateur·rice·s bloqué·e·s"
|
||||||
|
@ -384,7 +387,7 @@ antennaSource: "Source de l’antenne"
|
||||||
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 d’utilisateur·rice par ligne"
|
antennaUsersDescription: "Saisissez un seul nom d’utilisateur·rice par ligne"
|
||||||
|
@ -927,6 +930,7 @@ remoteOnly: "Distant uniquement"
|
||||||
failedToUpload: "Échec du transfert"
|
failedToUpload: "Échec du transfert"
|
||||||
cannotUploadBecauseInappropriate: "Impossible de télécharger le document car il a été déterminé qu'il pouvait contenir un contenu inapproprié."
|
cannotUploadBecauseInappropriate: "Impossible de télécharger le document car il a été déterminé qu'il pouvait contenir un contenu inapproprié."
|
||||||
cannotUploadBecauseNoFreeSpace: "Impossible de télécharger en raison d'un manque d'espace libre sur le disque.\n"
|
cannotUploadBecauseNoFreeSpace: "Impossible de télécharger en raison d'un manque d'espace libre sur le disque.\n"
|
||||||
|
cannotUploadBecauseExceedsFileSizeLimit: "Ce fichier ne peut pas être téléchargé parce qu'il dépasse la taille maximale."
|
||||||
beta: "Bêta"
|
beta: "Bêta"
|
||||||
enableAutoSensitive: "Détermination automatique de NSFW"
|
enableAutoSensitive: "Détermination automatique de NSFW"
|
||||||
enableAutoSensitiveDescription: "S'il est disponible, le drapeau NSFW est automatiquement défini sur le média en utilisant l'apprentissage automatique. Même si cette fonction est désactivée, elle peut être réglée automatiquement dans certains cas."
|
enableAutoSensitiveDescription: "S'il est disponible, le drapeau NSFW est automatiquement défini sur le média en utilisant l'apprentissage automatique. Même si cette fonction est désactivée, elle peut être réglée automatiquement dans certains cas."
|
||||||
|
@ -948,12 +952,14 @@ caption: "Libellé"
|
||||||
loggedInAsBot: "Connecté actuellement en tant que bot"
|
loggedInAsBot: "Connecté actuellement en tant que bot"
|
||||||
tools: "Outils"
|
tools: "Outils"
|
||||||
cannotLoad: "Chargement impossible"
|
cannotLoad: "Chargement impossible"
|
||||||
|
numberOfProfileView: "Nombre de vues du profil"
|
||||||
like: "J'aime"
|
like: "J'aime"
|
||||||
unlike: "Ne plus aimer"
|
unlike: "Ne plus aimer"
|
||||||
numberOfLikes: "Favoris"
|
numberOfLikes: "Favoris"
|
||||||
show: "Affichage"
|
show: "Affichage"
|
||||||
neverShow: "Ne plus afficher"
|
neverShow: "Ne plus afficher"
|
||||||
remindMeLater: "Peut-être plus tard"
|
remindMeLater: "Peut-être plus tard"
|
||||||
|
didYouLikeMisskey: "Avez-vous aimé Misskey ?"
|
||||||
roles: "Rôles"
|
roles: "Rôles"
|
||||||
role: "Rôles"
|
role: "Rôles"
|
||||||
noRole: "Aucun rôle"
|
noRole: "Aucun rôle"
|
||||||
|
@ -963,9 +969,13 @@ assign: "Attribuer"
|
||||||
unassign: "Retirer"
|
unassign: "Retirer"
|
||||||
color: "Couleur"
|
color: "Couleur"
|
||||||
manageCustomEmojis: "Gestion des émojis personnalisés"
|
manageCustomEmojis: "Gestion des émojis personnalisés"
|
||||||
|
youCannotCreateAnymore: "Vous avez atteint la limite de création."
|
||||||
|
cannotPerformTemporary: "Temporairement indisponible"
|
||||||
|
invalidParamError: "Paramètres invalides"
|
||||||
preset: "Préréglage"
|
preset: "Préréglage"
|
||||||
selectFromPresets: "Sélectionner à partir des préréglages"
|
selectFromPresets: "Sélectionner à partir des préréglages"
|
||||||
achievements: "Accomplissements"
|
achievements: "Accomplissements"
|
||||||
|
gotInvalidResponseError: "Réponse du serveur invalide"
|
||||||
thisPostMayBeAnnoying: "Cette note peut gêner d'autres personnes."
|
thisPostMayBeAnnoying: "Cette note peut gêner d'autres personnes."
|
||||||
thisPostMayBeAnnoyingHome: "Publier vers le fil principal"
|
thisPostMayBeAnnoyingHome: "Publier vers le fil principal"
|
||||||
thisPostMayBeAnnoyingCancel: "Annuler"
|
thisPostMayBeAnnoyingCancel: "Annuler"
|
||||||
|
@ -975,11 +985,15 @@ internalServerError: "Erreur interne du serveur"
|
||||||
copyErrorInfo: "Copier les détails de l’erreur"
|
copyErrorInfo: "Copier les détails de l’erreur"
|
||||||
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"
|
||||||
|
noteIdOrUrl: "Identifiant de la note ou URL"
|
||||||
video: "Vidéo"
|
video: "Vidéo"
|
||||||
videos: "Vidéos"
|
videos: "Vidéos"
|
||||||
dataSaver: "Économiseur de données"
|
dataSaver: "Économiseur de données"
|
||||||
|
@ -987,6 +1001,7 @@ accountMigration: "Migration de compte"
|
||||||
accountMoved: "Cet·te utilisateur·rice a migré son compte vers :"
|
accountMoved: "Cet·te utilisateur·rice a migré son compte vers :"
|
||||||
accountMovedShort: "Ce compte a migré"
|
accountMovedShort: "Ce compte a migré"
|
||||||
operationForbidden: "Opération non autorisée"
|
operationForbidden: "Opération non autorisée"
|
||||||
|
forceShowAds: "Toujours afficher les publicités"
|
||||||
addMemo: "Ajouter un mémo"
|
addMemo: "Ajouter un mémo"
|
||||||
reactionsList: "Réactions"
|
reactionsList: "Réactions"
|
||||||
renotesList: "Liste de renotes"
|
renotesList: "Liste de renotes"
|
||||||
|
@ -995,23 +1010,35 @@ leftTop: "En haut à gauche"
|
||||||
rightTop: "En haut à droite"
|
rightTop: "En haut à droite"
|
||||||
leftBottom: "En bas à gauche"
|
leftBottom: "En bas à gauche"
|
||||||
rightBottom: "En bas à droite"
|
rightBottom: "En bas à droite"
|
||||||
|
stackAxis: "Direction d'empilement"
|
||||||
vertical: "Vertical"
|
vertical: "Vertical"
|
||||||
horizontal: "Latéral"
|
horizontal: "Latéral"
|
||||||
|
position: "Position"
|
||||||
serverRules: "Règles du serveur"
|
serverRules: "Règles du serveur"
|
||||||
|
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 d’expiration"
|
expirationDate: "Date d’expiration"
|
||||||
|
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 d’accord ?"
|
doYouAgree: "Êtes-vous d’accord ?"
|
||||||
|
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"
|
||||||
|
@ -1531,7 +1558,7 @@ _visibility:
|
||||||
_postForm:
|
_postForm:
|
||||||
replyPlaceholder: "Répondre à cette note ..."
|
replyPlaceholder: "Répondre à cette note ..."
|
||||||
quotePlaceholder: "Citez cette note ..."
|
quotePlaceholder: "Citez cette note ..."
|
||||||
channelPlaceholder: "Publier vers le canal"
|
channelPlaceholder: "Publier au canal…"
|
||||||
_placeholders:
|
_placeholders:
|
||||||
a: "Quoi de neuf ?"
|
a: "Quoi de neuf ?"
|
||||||
b: "Il s'est passé quelque chose ?"
|
b: "Il s'est passé quelque chose ?"
|
||||||
|
|
|
@ -198,6 +198,7 @@ export interface Locale {
|
||||||
"perDay": string;
|
"perDay": string;
|
||||||
"stopActivityDelivery": string;
|
"stopActivityDelivery": string;
|
||||||
"blockThisInstance": string;
|
"blockThisInstance": string;
|
||||||
|
"silenceThisInstance": string;
|
||||||
"operations": string;
|
"operations": string;
|
||||||
"software": string;
|
"software": string;
|
||||||
"version": string;
|
"version": string;
|
||||||
|
@ -217,6 +218,8 @@ export interface Locale {
|
||||||
"clearCachedFilesConfirm": string;
|
"clearCachedFilesConfirm": string;
|
||||||
"blockedInstances": string;
|
"blockedInstances": string;
|
||||||
"blockedInstancesDescription": string;
|
"blockedInstancesDescription": string;
|
||||||
|
"silencedInstances": string;
|
||||||
|
"silencedInstancesDescription": string;
|
||||||
"muteAndBlock": string;
|
"muteAndBlock": string;
|
||||||
"mutedUsers": string;
|
"mutedUsers": string;
|
||||||
"blockedUsers": string;
|
"blockedUsers": string;
|
||||||
|
@ -534,6 +537,7 @@ export interface Locale {
|
||||||
"deleteAll": string;
|
"deleteAll": string;
|
||||||
"showFixedPostForm": string;
|
"showFixedPostForm": string;
|
||||||
"showFixedPostFormInChannel": string;
|
"showFixedPostFormInChannel": string;
|
||||||
|
"withRepliesByDefaultForNewlyFollowed": string;
|
||||||
"newNoteRecived": string;
|
"newNoteRecived": string;
|
||||||
"sounds": string;
|
"sounds": string;
|
||||||
"sound": string;
|
"sound": string;
|
||||||
|
@ -2059,6 +2063,7 @@ export interface Locale {
|
||||||
"userLists": string;
|
"userLists": string;
|
||||||
"excludeMutingUsers": string;
|
"excludeMutingUsers": string;
|
||||||
"excludeInactiveUsers": string;
|
"excludeInactiveUsers": string;
|
||||||
|
"withReplies": string;
|
||||||
};
|
};
|
||||||
"_charts": {
|
"_charts": {
|
||||||
"federation": string;
|
"federation": string;
|
||||||
|
|
|
@ -110,14 +110,14 @@ 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"
|
||||||
pinned: "Fissa sul profilo"
|
pinned: "Fissa sul profilo"
|
||||||
you: "Tu"
|
you: "Tu"
|
||||||
clickToShow: "Clicca per visualizzare"
|
clickToShow: "Contenuto occultato, cliccare solo se si intende vedere"
|
||||||
sensitive: "Esplicito"
|
sensitive: "Allegato esplicito"
|
||||||
add: "Aggiungi"
|
add: "Aggiungi"
|
||||||
reaction: "Reazioni"
|
reaction: "Reazioni"
|
||||||
reactions: "Reazioni"
|
reactions: "Reazioni"
|
||||||
|
@ -195,6 +195,7 @@ perHour: "orario"
|
||||||
perDay: "giornaliero"
|
perDay: "giornaliero"
|
||||||
stopActivityDelivery: "Interrompi la distribuzione di attività"
|
stopActivityDelivery: "Interrompi la distribuzione di attività"
|
||||||
blockThisInstance: "Blocca questa istanza"
|
blockThisInstance: "Blocca questa istanza"
|
||||||
|
silenceThisInstance: "Silenzia l'istanza"
|
||||||
operations: "Operazioni"
|
operations: "Operazioni"
|
||||||
software: "Software"
|
software: "Software"
|
||||||
version: "Versione"
|
version: "Versione"
|
||||||
|
@ -214,6 +215,8 @@ clearCachedFiles: "Svuota cache"
|
||||||
clearCachedFilesConfirm: "Vuoi davvero svuotare la cache da tutti i file remoti?"
|
clearCachedFilesConfirm: "Vuoi davvero svuotare la cache da tutti i file remoti?"
|
||||||
blockedInstances: "Istanze bloccate"
|
blockedInstances: "Istanze bloccate"
|
||||||
blockedInstancesDescription: "Elenca le istanze che vuoi bloccare, una per riga. Esse non potranno più interagire con la tua istanza."
|
blockedInstancesDescription: "Elenca le istanze che vuoi bloccare, una per riga. Esse non potranno più interagire con la tua istanza."
|
||||||
|
silencedInstances: "Istanze silenziate"
|
||||||
|
silencedInstancesDescription: "Elenca i nomi host delle istanze che vuoi silenziare. Tutti i profili nelle istanze silenziate vengono trattati come tali. Possono solo inviare richieste di follow e menzionare soltanto i profili locali che seguono. Le istanze bloccate non sono interessate."
|
||||||
muteAndBlock: "Silenziati / Bloccati"
|
muteAndBlock: "Silenziati / Bloccati"
|
||||||
mutedUsers: "Profili silenziati"
|
mutedUsers: "Profili silenziati"
|
||||||
blockedUsers: "Profili bloccati"
|
blockedUsers: "Profili bloccati"
|
||||||
|
@ -278,7 +281,7 @@ agreeTo: "Sono d'accordo con {0}"
|
||||||
agree: "Accetto"
|
agree: "Accetto"
|
||||||
agreeBelow: "Accetto quanto riportato sotto"
|
agreeBelow: "Accetto quanto riportato sotto"
|
||||||
basicNotesBeforeCreateAccount: "Note importanti"
|
basicNotesBeforeCreateAccount: "Note importanti"
|
||||||
termsOfService: "Informativa ai sensi degli artt. 13 e 14 del Regolamento UE 2016/679 per la protezione dei dati personali (GDPR)"
|
termsOfService: "Condizioni d'uso del servizio"
|
||||||
start: "Inizia!"
|
start: "Inizia!"
|
||||||
home: "Home"
|
home: "Home"
|
||||||
remoteUserCaution: "Le informazioni potrebbero essere incomplete poiché questo profilo remoto potrebbe non essere completamente federato."
|
remoteUserCaution: "Le informazioni potrebbero essere incomplete poiché questo profilo remoto potrebbe non essere completamente federato."
|
||||||
|
@ -531,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"
|
||||||
|
@ -1132,9 +1136,9 @@ externalServices: "Servizi esterni"
|
||||||
impressum: "Dichiarazione di proprietà"
|
impressum: "Dichiarazione di proprietà"
|
||||||
impressumUrl: "URL della dichiarazione di proprietà"
|
impressumUrl: "URL della dichiarazione di proprietà"
|
||||||
impressumDescription: "La dichiarazione di proprietà, è obbligatoria in alcuni paesi come la Germania (Impressum)."
|
impressumDescription: "La dichiarazione di proprietà, è obbligatoria in alcuni paesi come la Germania (Impressum)."
|
||||||
privacyPolicy: "Informativa sulla privacy"
|
privacyPolicy: "Informativa ai sensi degli artt. 13 e 14 del Regolamento UE 2016/679 per la protezione dei dati personali (GDPR)"
|
||||||
privacyPolicyUrl: "URL della informativa privacy"
|
privacyPolicyUrl: "URL della informativa privacy"
|
||||||
tosAndPrivacyPolicy: "Condizioni d'uso e informativa sulla privacy"
|
tosAndPrivacyPolicy: "Condizioni d'uso e informativa privacy"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "Solo ai profili attuali"
|
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."
|
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."
|
||||||
|
@ -1879,9 +1883,9 @@ _poll:
|
||||||
remainingSeconds: "Rimangono {s} secondi"
|
remainingSeconds: "Rimangono {s} secondi"
|
||||||
_visibility:
|
_visibility:
|
||||||
public: "Pubblica"
|
public: "Pubblica"
|
||||||
publicDescription: "Visibile per tutti sul Fediverso"
|
publicDescription: "Visibilità pubblica"
|
||||||
home: "Home"
|
home: "Home"
|
||||||
homeDescription: "Visibile solo sulla timeline locale"
|
homeDescription: "Visibile solo nella Home"
|
||||||
followers: "Follower"
|
followers: "Follower"
|
||||||
followersDescription: "Visibile solo ai tuoi follower"
|
followersDescription: "Visibile solo ai tuoi follower"
|
||||||
specified: "Nota diretta"
|
specified: "Nota diretta"
|
||||||
|
@ -1921,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"
|
||||||
|
@ -2085,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})"
|
||||||
|
@ -2141,3 +2146,11 @@ _moderationLogTypes:
|
||||||
createAd: "Banner creato"
|
createAd: "Banner creato"
|
||||||
deleteAd: "Banner eliminato"
|
deleteAd: "Banner eliminato"
|
||||||
updateAd: "Banner aggiornato"
|
updateAd: "Banner aggiornato"
|
||||||
|
_fileViewer:
|
||||||
|
title: "Dettagli del file"
|
||||||
|
type: "Tipo di file"
|
||||||
|
size: "Dimensioni file"
|
||||||
|
url: "URL"
|
||||||
|
uploadedAt: "Caricato il"
|
||||||
|
attachedNotes: "Note a cui è allegato"
|
||||||
|
thisPageCanBeSeenFromTheAuthor: "Questa pagina può essere vista solo da chi ha caricato il file."
|
||||||
|
|
|
@ -195,6 +195,7 @@ perHour: "1時間ごと"
|
||||||
perDay: "1日ごと"
|
perDay: "1日ごと"
|
||||||
stopActivityDelivery: "アクティビティの配送を停止"
|
stopActivityDelivery: "アクティビティの配送を停止"
|
||||||
blockThisInstance: "このサーバーをブロック"
|
blockThisInstance: "このサーバーをブロック"
|
||||||
|
silenceThisInstance: "サーバーをサイレンス"
|
||||||
operations: "操作"
|
operations: "操作"
|
||||||
software: "ソフトウェア"
|
software: "ソフトウェア"
|
||||||
version: "バージョン"
|
version: "バージョン"
|
||||||
|
@ -213,7 +214,9 @@ clearQueueConfirmText: "未配達の投稿は配送されなくなります。
|
||||||
clearCachedFiles: "キャッシュをクリア"
|
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: "フォローする際、デフォルトで返信をTLに含むようにする"
|
||||||
newNoteRecived: "新しいノートがあります"
|
newNoteRecived: "新しいノートがあります"
|
||||||
sounds: "サウンド"
|
sounds: "サウンド"
|
||||||
sound: "サウンド"
|
sound: "サウンド"
|
||||||
|
@ -589,7 +593,7 @@ poll: "アンケート"
|
||||||
useCw: "内容を隠す"
|
useCw: "内容を隠す"
|
||||||
enablePlayer: "プレイヤーを開く"
|
enablePlayer: "プレイヤーを開く"
|
||||||
disablePlayer: "プレイヤーを閉じる"
|
disablePlayer: "プレイヤーを閉じる"
|
||||||
expandTweet: "ツイートを展開する"
|
expandTweet: "ポストを展開する"
|
||||||
themeEditor: "テーマエディター"
|
themeEditor: "テーマエディター"
|
||||||
description: "説明"
|
description: "説明"
|
||||||
describeFile: "キャプションを付ける"
|
describeFile: "キャプションを付ける"
|
||||||
|
@ -794,7 +798,7 @@ active: "アクティブ"
|
||||||
offline: "オフライン"
|
offline: "オフライン"
|
||||||
notRecommended: "非推奨"
|
notRecommended: "非推奨"
|
||||||
botProtection: "Botプロテクション"
|
botProtection: "Botプロテクション"
|
||||||
instanceBlocking: "サーバーブロック"
|
instanceBlocking: "サーバーブロック・サイレンス"
|
||||||
selectAccount: "アカウントを選択"
|
selectAccount: "アカウントを選択"
|
||||||
switchAccount: "アカウントを切り替え"
|
switchAccount: "アカウントを切り替え"
|
||||||
enabled: "有効"
|
enabled: "有効"
|
||||||
|
@ -1974,6 +1978,7 @@ _exportOrImport:
|
||||||
userLists: "リスト"
|
userLists: "リスト"
|
||||||
excludeMutingUsers: "ミュートしているユーザーを除外"
|
excludeMutingUsers: "ミュートしているユーザーを除外"
|
||||||
excludeInactiveUsers: "使われていないアカウントを除外"
|
excludeInactiveUsers: "使われていないアカウントを除外"
|
||||||
|
withReplies: "インポートした人による返信をTLに含むようにする"
|
||||||
|
|
||||||
_charts:
|
_charts:
|
||||||
federation: "連合"
|
federation: "連合"
|
||||||
|
|
|
@ -586,7 +586,7 @@ poll: "アンケート"
|
||||||
useCw: "内容を隠す"
|
useCw: "内容を隠す"
|
||||||
enablePlayer: "プレイヤーを開く"
|
enablePlayer: "プレイヤーを開く"
|
||||||
disablePlayer: "プレイヤーを閉じる"
|
disablePlayer: "プレイヤーを閉じる"
|
||||||
expandTweet: "ツイートを展開する"
|
expandTweet: "ポストを展開する"
|
||||||
themeEditor: "テーマエディター"
|
themeEditor: "テーマエディター"
|
||||||
description: "説明"
|
description: "説明"
|
||||||
describeFile: "キャプションを付ける"
|
describeFile: "キャプションを付ける"
|
||||||
|
|
|
@ -589,7 +589,7 @@ poll: "투표"
|
||||||
useCw: "내용 숨기기"
|
useCw: "내용 숨기기"
|
||||||
enablePlayer: "플레이어 열기"
|
enablePlayer: "플레이어 열기"
|
||||||
disablePlayer: "플레이어 닫기"
|
disablePlayer: "플레이어 닫기"
|
||||||
expandTweet: "트윗 확장하기"
|
expandTweet: "게시물 확장하기"
|
||||||
themeEditor: "테마 에디터"
|
themeEditor: "테마 에디터"
|
||||||
description: "설명"
|
description: "설명"
|
||||||
describeFile: "캡션 추가"
|
describeFile: "캡션 추가"
|
||||||
|
|
|
@ -1117,15 +1117,25 @@ keepScreenOn: "เปิดหน้าจอไว้"
|
||||||
notifyNotes: "แจ้งเตือนเกี่ยวกับโพสต์ใหม่"
|
notifyNotes: "แจ้งเตือนเกี่ยวกับโพสต์ใหม่"
|
||||||
unnotifyNotes: "หยุดการแจ้งเตือนเกี่ยวกับโน้ตใหม่"
|
unnotifyNotes: "หยุดการแจ้งเตือนเกี่ยวกับโน้ตใหม่"
|
||||||
authentication: "การตรวจสอบสิทธิ์"
|
authentication: "การตรวจสอบสิทธิ์"
|
||||||
|
authenticationRequiredToContinue: "กรุณาตรวจสอบการรับรองความถูกต้องเพื่อดำเนินการต่อ"
|
||||||
dateAndTime: "เวลาประทับ"
|
dateAndTime: "เวลาประทับ"
|
||||||
showRenotes: "แสดงรีโน้ต"
|
showRenotes: "แสดงรีโน้ต"
|
||||||
edited: "แก้ไขแล้ว"
|
edited: "แก้ไขแล้ว"
|
||||||
notificationRecieveConfig: "การตั้งค่าการแจ้งเตือน"
|
notificationRecieveConfig: "การตั้งค่าการแจ้งเตือน"
|
||||||
mutualFollow: "ติดตามซึ่งกันและกัน"
|
mutualFollow: "ติดตามซึ่งกันและกัน"
|
||||||
fileAttachedOnly: "เฉพาะโน้ตที่มีไฟล์เท่านั้น"
|
fileAttachedOnly: "เฉพาะโน้ตที่มีไฟล์เท่านั้น"
|
||||||
|
showRepliesToOthersInTimeline: "แสดงการตอบกลับไปยังอื่นๆในไทม์ไลน์"
|
||||||
|
hideRepliesToOthersInTimeline: "ซ่อนการตอบกลับไปยังอื่นๆจากไทม์ไลน์"
|
||||||
|
externalServices: "บริการภายนอก"
|
||||||
|
impressum: "อิมเพรสชั่น"
|
||||||
|
impressumUrl: "URL อิมเพรสชั่น"
|
||||||
|
privacyPolicy: "นโยบายความเป็นส่วนตัว"
|
||||||
|
privacyPolicyUrl: "URL นโยบายความเป็นส่วนตัว"
|
||||||
|
tosAndPrivacyPolicy: "เงื่อนไขในการให้บริการและนโยบายความเป็นส่วนตัว"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น"
|
forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น"
|
||||||
forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน"
|
forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน"
|
||||||
|
needConfirmationToRead: "จำเป็นต้องยืนยันเพื่อทำเครื่องหมายบอกว่าอ่านแล้ว"
|
||||||
needConfirmationToReadDescription: "ข้อความแจ้งแยก ถ้าหากต้องการเพื่อยืนยันว่ากำลังทำเครื่องหมายประกาศนี้ว่าอ่านแล้วจะแสดงขึ้นถ้าหากเปิดใช้งาน การประกาศนั้นจะไม่รวมอยู่ในฟังก์ชั่นว่า \"ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว\""
|
needConfirmationToReadDescription: "ข้อความแจ้งแยก ถ้าหากต้องการเพื่อยืนยันว่ากำลังทำเครื่องหมายประกาศนี้ว่าอ่านแล้วจะแสดงขึ้นถ้าหากเปิดใช้งาน การประกาศนั้นจะไม่รวมอยู่ในฟังก์ชั่นว่า \"ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว\""
|
||||||
end: "ประกาศเก็บถาวร"
|
end: "ประกาศเก็บถาวร"
|
||||||
tooManyActiveAnnouncementDescription: "การมีประกาศที่ใช้งานมากเกินไปนั้นอาจจะทำให้ประสบการณ์ของผู้ใช้งานนั้นดูแย่ลง โปรดกรุณาพิจารณาการเก็บประกาศที่ล้าสมัยด้วยนะค่ะ"
|
tooManyActiveAnnouncementDescription: "การมีประกาศที่ใช้งานมากเกินไปนั้นอาจจะทำให้ประสบการณ์ของผู้ใช้งานนั้นดูแย่ลง โปรดกรุณาพิจารณาการเก็บประกาศที่ล้าสมัยด้วยนะค่ะ"
|
||||||
|
@ -1150,6 +1160,8 @@ _serverRules:
|
||||||
description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ"
|
description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ"
|
||||||
_serverSettings:
|
_serverSettings:
|
||||||
iconUrl: "ไอคอน URL"
|
iconUrl: "ไอคอน URL"
|
||||||
|
appIconUsageExample: "E.g. เป็น PWA หรือเมื่อแสดงผลเป็นบุ๊กมาร์กหน้าจอหลักบนโทรศัพท์"
|
||||||
|
appIconResolutionMustBe: "ความละเอียดขั้นต่ำไว้คือ {resolution}."
|
||||||
manifestJsonOverride: "manifest.json โอเวอร์ลาย"
|
manifestJsonOverride: "manifest.json โอเวอร์ลาย"
|
||||||
shortName: "ชื่อย่อ"
|
shortName: "ชื่อย่อ"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
|
@ -1515,6 +1527,8 @@ _ad:
|
||||||
reduceFrequencyOfThisAd: "แสดงโฆษณานี้ให้น้อยลง"
|
reduceFrequencyOfThisAd: "แสดงโฆษณานี้ให้น้อยลง"
|
||||||
hide: "ไม่ต้องแสดง"
|
hide: "ไม่ต้องแสดง"
|
||||||
timezoneinfo: "วันในสัปดาห์นี้จะถูกกำหนดจากโซนเวลาของเซิร์ฟเวอร์"
|
timezoneinfo: "วันในสัปดาห์นี้จะถูกกำหนดจากโซนเวลาของเซิร์ฟเวอร์"
|
||||||
|
adsSettings: "ตั้งค่าการโฆษณา"
|
||||||
|
setZeroToDisable: "ตั้งค่านี้ให้เป็น 0 เพื่อปิดใช้งานโฆษณาอัปเดตแบบเรียลไทม์"
|
||||||
_forgotPassword:
|
_forgotPassword:
|
||||||
enterEmail: "ป้อนที่อยู่อีเมลที่คุณเคยใช้ในการลงทะเบียนไว้ ลิงก์ที่คุณสามารถรีเซ็ตรหัสผ่านได้นั้นจะถูกส่งไปนะ"
|
enterEmail: "ป้อนที่อยู่อีเมลที่คุณเคยใช้ในการลงทะเบียนไว้ ลิงก์ที่คุณสามารถรีเซ็ตรหัสผ่านได้นั้นจะถูกส่งไปนะ"
|
||||||
ifNoEmail: "ถ้าหากคุณไม่ได้ใช้อีเมลระหว่างการลงทะเบียน กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์แทนนะ"
|
ifNoEmail: "ถ้าหากคุณไม่ได้ใช้อีเมลระหว่างการลงทะเบียน กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์แทนนะ"
|
||||||
|
@ -1714,6 +1728,7 @@ _2fa:
|
||||||
step1: "ขั้นตอนแรก ติดตั้งแอปยืนยันตัวตน (เช่น {a} หรือ {b}) บนอุปกรณ์ของคุณ"
|
step1: "ขั้นตอนแรก ติดตั้งแอปยืนยันตัวตน (เช่น {a} หรือ {b}) บนอุปกรณ์ของคุณ"
|
||||||
step2: "จากนั้นสแกนรหัส QR ที่แสดงบนหน้าจอนี้"
|
step2: "จากนั้นสแกนรหัส QR ที่แสดงบนหน้าจอนี้"
|
||||||
step2Click: "การคลิกที่รหัส QR นี้จะช่วยให้คุณนั้นสามารถลงทะเบียน 2FA กับคีย์ความปลอดภัยหรือแอปตรวจสอบความถูกต้องของโทรศัพท์ได้"
|
step2Click: "การคลิกที่รหัส QR นี้จะช่วยให้คุณนั้นสามารถลงทะเบียน 2FA กับคีย์ความปลอดภัยหรือแอปตรวจสอบความถูกต้องของโทรศัพท์ได้"
|
||||||
|
step2Uri: "ป้อนใส่ URL ดังต่อไปนี้ถ้าหากคุณใช้โปรแกรมเดสก์ท็อป"
|
||||||
step3Title: "ป้อนรหัสยืนยัน"
|
step3Title: "ป้อนรหัสยืนยัน"
|
||||||
step3: "ป้อนโทเค็นที่แอปของคุณให้มาเพื่อเสร็จสิ้นการตั้งค่า"
|
step3: "ป้อนโทเค็นที่แอปของคุณให้มาเพื่อเสร็จสิ้นการตั้งค่า"
|
||||||
setupCompleted: "ตั้งค่าสำเร็จแล้ว"
|
setupCompleted: "ตั้งค่าสำเร็จแล้ว"
|
||||||
|
@ -1732,6 +1747,8 @@ _2fa:
|
||||||
renewTOTPOk: "ตั้งค่าคอนฟิกใหม่"
|
renewTOTPOk: "ตั้งค่าคอนฟิกใหม่"
|
||||||
renewTOTPCancel: "ไม่เป็นไร"
|
renewTOTPCancel: "ไม่เป็นไร"
|
||||||
backupCodes: "รหัสสำรองข้อมูล"
|
backupCodes: "รหัสสำรองข้อมูล"
|
||||||
|
backupCodeUsedWarning: "มีการใช้รหัสสำรองแล้ว โปรดกรุณากำหนดค่าการตรวจสอบสิทธิ์แบบสองปัจจัยโดยเร็วที่สุดถ้าหากคุณยังไม่สามารถใช้งานได้อีก"
|
||||||
|
backupCodesExhaustedWarning: "รหัสสำรองทั้งหมดถูกใช้แล้ว ถ้าหากคุณยังสูญเสียการเข้าถึงแอปการตรวจสอบสิทธิ์แบบสองปัจจัยคุณจะยังไม่สามารถเข้าถึงบัญชีนี้ได้ กรุณากำหนดค่าการรับรองความถูกต้องด้วยการยืนยันสองชั้น"
|
||||||
_permissions:
|
_permissions:
|
||||||
"read:account": "ดูข้อมูลบัญชีของคุณ"
|
"read:account": "ดูข้อมูลบัญชีของคุณ"
|
||||||
"write:account": "แก้ไขข้อมูลบัญชีของคุณ"
|
"write:account": "แก้ไขข้อมูลบัญชีของคุณ"
|
||||||
|
@ -1894,6 +1911,7 @@ _exportOrImport:
|
||||||
userLists: "รายการ"
|
userLists: "รายการ"
|
||||||
excludeMutingUsers: "ยกเว้นผู้ใช้ที่ปิดเสียง"
|
excludeMutingUsers: "ยกเว้นผู้ใช้ที่ปิดเสียง"
|
||||||
excludeInactiveUsers: "ยกเว้นผู้ใช้ที่ไม่ได้ใช้งาน"
|
excludeInactiveUsers: "ยกเว้นผู้ใช้ที่ไม่ได้ใช้งาน"
|
||||||
|
withReplies: "รวมการตอบกลับจากผู้ใช้ที่นำเข้าไว้ในไทม์ไลน์"
|
||||||
_charts:
|
_charts:
|
||||||
federation: "สหพันธ์"
|
federation: "สหพันธ์"
|
||||||
apRequest: "คำขอ"
|
apRequest: "คำขอ"
|
||||||
|
@ -2114,3 +2132,11 @@ _moderationLogTypes:
|
||||||
createAd: "สร้างโฆษณาแล้ว"
|
createAd: "สร้างโฆษณาแล้ว"
|
||||||
deleteAd: "ลบโฆษณาออกแล้ว"
|
deleteAd: "ลบโฆษณาออกแล้ว"
|
||||||
updateAd: "อัปเดตโฆษณาแล้ว"
|
updateAd: "อัปเดตโฆษณาแล้ว"
|
||||||
|
_fileViewer:
|
||||||
|
title: "รายละเอียดไฟล์"
|
||||||
|
type: "ประเภทไฟล์"
|
||||||
|
size: "ขนาดไฟล์"
|
||||||
|
url: "URL"
|
||||||
|
uploadedAt: "วันที่เข้าร่วม"
|
||||||
|
attachedNotes: "โน้ตที่แนบมาด้วย"
|
||||||
|
thisPageCanBeSeenFromTheAuthor: "หน้าเพจนี้จะสามารถปรากฏได้โดยผู้ใช้ที่อัปโหลดไฟล์นี้เท่านั้น"
|
||||||
|
|
|
@ -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: "音效"
|
||||||
|
@ -1125,8 +1129,8 @@ showRenotes: "顯示轉發貼文"
|
||||||
edited: "已編輯"
|
edited: "已編輯"
|
||||||
notificationRecieveConfig: "接受通知的設定"
|
notificationRecieveConfig: "接受通知的設定"
|
||||||
mutualFollow: "互相追隨"
|
mutualFollow: "互相追隨"
|
||||||
fileAttachedOnly: "包含附件"
|
fileAttachedOnly: "顯示包含附件的貼文"
|
||||||
showRepliesToOthersInTimeline: "在時間軸上顯示給其他人的回覆"
|
showRepliesToOthersInTimeline: "顯示給其他人的回覆"
|
||||||
hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆"
|
hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆"
|
||||||
externalServices: "外部服務"
|
externalServices: "外部服務"
|
||||||
impressum: "營運者資訊"
|
impressum: "營運者資訊"
|
||||||
|
@ -1921,6 +1925,7 @@ _exportOrImport:
|
||||||
userLists: "清單"
|
userLists: "清單"
|
||||||
excludeMutingUsers: "排除被靜音的使用者"
|
excludeMutingUsers: "排除被靜音的使用者"
|
||||||
excludeInactiveUsers: "排除不活躍帳戶"
|
excludeInactiveUsers: "排除不活躍帳戶"
|
||||||
|
withReplies: "將被匯入的追隨中清單的貼文回覆包含在時間軸"
|
||||||
_charts:
|
_charts:
|
||||||
federation: "聯邦宇宙"
|
federation: "聯邦宇宙"
|
||||||
apRequest: "請求"
|
apRequest: "請求"
|
||||||
|
|
12
package.json
12
package.json
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2023.10.123",
|
"version": "2023.10.2-beta.2",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/misskey-dev/misskey.git"
|
"url": "https://github.com/misskey-dev/misskey.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.8.0",
|
"packageManager": "pnpm@8.9.2",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/frontend",
|
"packages/frontend",
|
||||||
"packages/backend",
|
"packages/backend",
|
||||||
|
@ -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.7.5",
|
"@typescript-eslint/eslint-plugin": "6.8.0",
|
||||||
"@typescript-eslint/parser": "6.7.5",
|
"@typescript-eslint/parser": "6.8.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.3.0",
|
"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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class InstanceSilence1697247230117 {
|
||||||
|
name = 'InstanceSilence1697247230117'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "silencedHosts" character varying(1024) array NOT NULL DEFAULT '{}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
export class FollowRequestWithReplies1697441463087 {
|
||||||
|
name = 'FollowRequestWithReplies1697441463087'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "follow_request" ADD "withReplies" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "follow_request" DROP COLUMN "withReplies"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,9 +59,9 @@
|
||||||
"@aws-sdk/client-s3": "3.412.0",
|
"@aws-sdk/client-s3": "3.412.0",
|
||||||
"@aws-sdk/lib-storage": "3.412.0",
|
"@aws-sdk/lib-storage": "3.412.0",
|
||||||
"@smithy/node-http-handler": "2.1.5",
|
"@smithy/node-http-handler": "2.1.5",
|
||||||
"@bull-board/api": "5.8.4",
|
"@bull-board/api": "5.9.1",
|
||||||
"@bull-board/fastify": "5.8.4",
|
"@bull-board/fastify": "5.9.1",
|
||||||
"@bull-board/ui": "5.8.4",
|
"@bull-board/ui": "5.9.1",
|
||||||
"@discordapp/twemoji": "14.1.2",
|
"@discordapp/twemoji": "14.1.2",
|
||||||
"@fastify/accepts": "4.2.0",
|
"@fastify/accepts": "4.2.0",
|
||||||
"@fastify/cookie": "9.1.0",
|
"@fastify/cookie": "9.1.0",
|
||||||
|
@ -75,10 +75,10 @@
|
||||||
"@nestjs/core": "10.2.7",
|
"@nestjs/core": "10.2.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.2.0",
|
"@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.92",
|
"@swc/core": "1.3.93",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
"archiver": "6.0.1",
|
"archiver": "6.0.1",
|
||||||
|
@ -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.3",
|
"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.23.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",
|
||||||
|
@ -121,13 +121,13 @@
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
"ms": "3.0.0-canary.1",
|
"ms": "3.0.0-canary.1",
|
||||||
"nanoid": "5.0.1",
|
"nanoid": "5.0.2",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nodemailer": "6.9.6",
|
"nodemailer": "6.9.6",
|
||||||
"nsfwjs": "2.4.2",
|
"nsfwjs": "2.4.2",
|
||||||
"oauth": "0.10.0",
|
"oauth": "0.10.0",
|
||||||
"oauth2orize": "1.11.1",
|
"oauth2orize": "1.12.0",
|
||||||
"oauth2orize-pkce": "0.1.2",
|
"oauth2orize-pkce": "0.1.2",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"otpauth": "9.1.5",
|
"otpauth": "9.1.5",
|
||||||
|
@ -155,7 +155,7 @@
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"summaly": "github:misskey-dev/summaly",
|
"summaly": "github:misskey-dev/summaly",
|
||||||
"systeminformation": "5.21.11",
|
"systeminformation": "5.21.12",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.8",
|
"tsc-alias": "1.8.8",
|
||||||
|
@ -173,47 +173,47 @@
|
||||||
"@jest/globals": "29.7.0",
|
"@jest/globals": "29.7.0",
|
||||||
"@simplewebauthn/typescript-types": "8.0.0",
|
"@simplewebauthn/typescript-types": "8.0.0",
|
||||||
"@swc/jest": "0.2.29",
|
"@swc/jest": "0.2.29",
|
||||||
"@types/accepts": "1.3.5",
|
"@types/accepts": "1.3.6",
|
||||||
"@types/archiver": "5.3.3",
|
"@types/archiver": "5.3.4",
|
||||||
"@types/bcryptjs": "2.4.4",
|
"@types/bcryptjs": "2.4.5",
|
||||||
"@types/body-parser": "1.19.3",
|
"@types/body-parser": "1.19.4",
|
||||||
"@types/cbor": "6.0.0",
|
"@types/cbor": "6.0.0",
|
||||||
"@types/color-convert": "2.0.1",
|
"@types/color-convert": "2.0.2",
|
||||||
"@types/content-disposition": "0.5.6",
|
"@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.6",
|
"@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.4",
|
"@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.4",
|
"@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.1",
|
"@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.6",
|
"@types/ws": "8.5.8",
|
||||||
"@typescript-eslint/eslint-plugin": "6.7.5",
|
"@typescript-eslint/eslint-plugin": "6.8.0",
|
||||||
"@typescript-eslint/parser": "6.7.5",
|
"@typescript-eslint/parser": "6.8.0",
|
||||||
"aws-sdk-client-mock": "3.0.0",
|
"aws-sdk-client-mock": "3.0.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.51.0",
|
"eslint": "8.51.0",
|
||||||
|
|
|
@ -16,7 +16,7 @@ import type { AntennasRepository, UserListMembershipsRepository } from '@/models
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -39,7 +39,7 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private redisTimelineService: RedisTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
) {
|
) {
|
||||||
this.antennasFetched = false;
|
this.antennasFetched = false;
|
||||||
this.antennas = [];
|
this.antennas = [];
|
||||||
|
@ -84,7 +84,7 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
const redisPipeline = this.redisForTimelines.pipeline();
|
const redisPipeline = this.redisForTimelines.pipeline();
|
||||||
|
|
||||||
for (const antenna of matchedAntennas) {
|
for (const antenna of matchedAntennas) {
|
||||||
this.redisTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline);
|
this.funoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline);
|
||||||
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ import { FileInfoService } from './FileInfoService.js';
|
||||||
import { SearchService } from './SearchService.js';
|
import { SearchService } from './SearchService.js';
|
||||||
import { ClipService } from './ClipService.js';
|
import { ClipService } from './ClipService.js';
|
||||||
import { FeaturedService } from './FeaturedService.js';
|
import { FeaturedService } from './FeaturedService.js';
|
||||||
import { RedisTimelineService } from './RedisTimelineService.js';
|
import { FunoutTimelineService } from './FunoutTimelineService.js';
|
||||||
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
||||||
import FederationChart from './chart/charts/federation.js';
|
import FederationChart from './chart/charts/federation.js';
|
||||||
import NotesChart from './chart/charts/notes.js';
|
import NotesChart from './chart/charts/notes.js';
|
||||||
|
@ -190,7 +190,7 @@ const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: Fi
|
||||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
||||||
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
|
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
|
||||||
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
|
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
|
||||||
const $RedisTimelineService: Provider = { provide: 'RedisTimelineService', useExisting: RedisTimelineService };
|
const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService };
|
||||||
|
|
||||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
||||||
|
@ -323,7 +323,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
SearchService,
|
SearchService,
|
||||||
ClipService,
|
ClipService,
|
||||||
FeaturedService,
|
FeaturedService,
|
||||||
RedisTimelineService,
|
FunoutTimelineService,
|
||||||
ChartLoggerService,
|
ChartLoggerService,
|
||||||
FederationChart,
|
FederationChart,
|
||||||
NotesChart,
|
NotesChart,
|
||||||
|
@ -449,7 +449,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$SearchService,
|
$SearchService,
|
||||||
$ClipService,
|
$ClipService,
|
||||||
$FeaturedService,
|
$FeaturedService,
|
||||||
$RedisTimelineService,
|
$FunoutTimelineService,
|
||||||
$ChartLoggerService,
|
$ChartLoggerService,
|
||||||
$FederationChart,
|
$FederationChart,
|
||||||
$NotesChart,
|
$NotesChart,
|
||||||
|
@ -576,7 +576,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
SearchService,
|
SearchService,
|
||||||
ClipService,
|
ClipService,
|
||||||
FeaturedService,
|
FeaturedService,
|
||||||
RedisTimelineService,
|
FunoutTimelineService,
|
||||||
FederationChart,
|
FederationChart,
|
||||||
NotesChart,
|
NotesChart,
|
||||||
UsersChart,
|
UsersChart,
|
||||||
|
@ -701,7 +701,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$SearchService,
|
$SearchService,
|
||||||
$ClipService,
|
$ClipService,
|
||||||
$FeaturedService,
|
$FeaturedService,
|
||||||
$RedisTimelineService,
|
$FunoutTimelineService,
|
||||||
$FederationChart,
|
$FederationChart,
|
||||||
$NotesChart,
|
$NotesChart,
|
||||||
$UsersChart,
|
$UsersChart,
|
||||||
|
|
|
@ -353,7 +353,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||||
|
|
||||||
const queryOrNull = async () => (await this.emojisRepository.findOneBy({
|
const queryOrNull = async () => (await this.emojisRepository.findOneBy({
|
||||||
name,
|
name,
|
||||||
host: host ?? IsNull(),
|
host,
|
||||||
})) ?? null;
|
})) ?? null;
|
||||||
|
|
||||||
const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
|
const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RedisTimelineService {
|
export class FunoutTimelineService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisForTimelines)
|
@Inject(DI.redisForTimelines)
|
||||||
private redisForTimelines: Redis.Redis,
|
private redisForTimelines: Redis.Redis,
|
||||||
|
@ -77,4 +77,9 @@ export class RedisTimelineService {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public purge(name: string) {
|
||||||
|
return this.redisForTimelines.del('list:' + name);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -45,7 +45,7 @@ export class HashtagService {
|
||||||
await this.updateHashtag(user, tag, true, true);
|
await this.updateHashtag(user, tag, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const tag of (user.tags ?? []).filter(x => !tags.includes(x))) {
|
for (const tag of user.tags.filter(x => !tags.includes(x))) {
|
||||||
await this.updateHashtag(user, tag, true, false);
|
await this.updateHashtag(user, tag, true, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,9 @@ import { RoleService } from '@/core/RoleService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { SearchService } from '@/core/SearchService.js';
|
import { SearchService } from '@/core/SearchService.js';
|
||||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import { nyaize } from '@/misc/nyaize.js';
|
import { nyaize } from '@/misc/nyaize.js';
|
||||||
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
|
@ -196,7 +197,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private redisTimelineService: RedisTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
private noteReadService: NoteReadService,
|
private noteReadService: NoteReadService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
private relayService: RelayService,
|
private relayService: RelayService,
|
||||||
|
@ -215,6 +216,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private perUserNotesChart: PerUserNotesChart,
|
private perUserNotesChart: PerUserNotesChart,
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
|
private utilityService: UtilityService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -225,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) {
|
||||||
|
@ -250,8 +250,10 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (data.channel != null) data.visibleUsers = [];
|
if (data.channel != null) data.visibleUsers = [];
|
||||||
if (data.channel != null) data.localOnly = true;
|
if (data.channel != null) data.localOnly = true;
|
||||||
|
|
||||||
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
if (data.visibility === 'public' && data.channel == null) {
|
if (data.visibility === 'public' && data.channel == null) {
|
||||||
const sensitiveWords = (await this.metaService.fetch()).sensitiveWords;
|
const sensitiveWords = meta.sensitiveWords;
|
||||||
if (this.isSensitive(data, sensitiveWords)) {
|
if (this.isSensitive(data, sensitiveWords)) {
|
||||||
data.visibility = 'home';
|
data.visibility = 'home';
|
||||||
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
|
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
|
||||||
|
@ -259,6 +261,12 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
|
||||||
|
|
||||||
|
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
|
||||||
|
data.visibility = 'home';
|
||||||
|
}
|
||||||
|
|
||||||
if (data.renote) {
|
if (data.renote) {
|
||||||
switch (data.renote.visibility) {
|
switch (data.renote.visibility) {
|
||||||
case 'public':
|
case 'public':
|
||||||
|
@ -305,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 = 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;
|
||||||
}
|
}
|
||||||
|
@ -334,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)!))
|
||||||
|
@ -349,7 +338,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens);
|
mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32);
|
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
|
||||||
|
|
||||||
if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
|
if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
|
||||||
mentionedUsers.push(await this.usersRepository.findOneByOrFail({ id: data.reply!.userId }));
|
mentionedUsers.push(await this.usersRepository.findOneByOrFail({ id: data.reply!.userId }));
|
||||||
|
@ -574,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);
|
||||||
|
|
||||||
|
@ -841,9 +830,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
const r = this.redisForTimelines.pipeline();
|
const r = this.redisForTimelines.pipeline();
|
||||||
|
|
||||||
if (note.channelId) {
|
if (note.channelId) {
|
||||||
this.redisTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
|
this.funoutTimelineService.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);
|
this.funoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
||||||
|
|
||||||
const channelFollowings = await this.channelFollowingsRepository.find({
|
const channelFollowings = await this.channelFollowingsRepository.find({
|
||||||
where: {
|
where: {
|
||||||
|
@ -853,9 +842,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const channelFollowing of channelFollowings) {
|
for (const channelFollowing of channelFollowings) {
|
||||||
this.redisTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
this.funoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.redisTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
this.funoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -893,9 +882,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (!following.withReplies) continue;
|
if (!following.withReplies) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.redisTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
this.funoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.redisTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
this.funoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -911,36 +900,36 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (!userListMembership.withReplies) continue;
|
if (!userListMembership.withReplies) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.redisTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
|
this.funoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.redisTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
|
this.funoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
|
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
|
||||||
this.redisTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
this.funoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.redisTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
this.funoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自分自身以外への返信
|
// 自分自身以外への返信
|
||||||
if (note.replyId && note.replyUserId !== note.userId) {
|
if (note.replyId && note.replyUserId !== note.userId) {
|
||||||
this.redisTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
this.funoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
||||||
|
|
||||||
if (note.visibility === 'public' && note.userHost == null) {
|
if (note.visibility === 'public' && note.userHost == null) {
|
||||||
this.redisTimelineService.push('localTimelineWithReplies', note.id, 300, r);
|
this.funoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.redisTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
this.funoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.redisTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
|
this.funoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.visibility === 'public' && note.userHost == null) {
|
if (note.visibility === 'public' && note.userHost == null) {
|
||||||
this.redisTimelineService.push('localTimeline', note.id, 1000, r);
|
this.funoutTimelineService.push('localTimeline', note.id, 1000, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.redisTimelineService.push('localTimelineWithFiles', note.id, 500, r);
|
this.funoutTimelineService.push('localTimelineWithFiles', note.id, 500, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -237,10 +237,11 @@ export class QueueService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createImportFollowingJob(user: ThinUser, fileId: MiDriveFile['id']) {
|
public createImportFollowingJob(user: ThinUser, fileId: MiDriveFile['id'], withReplies?: boolean) {
|
||||||
return this.dbQueue.add('importFollowing', {
|
return this.dbQueue.add('importFollowing', {
|
||||||
user: { id: user.id },
|
user: { id: user.id },
|
||||||
fileId: fileId,
|
fileId: fileId,
|
||||||
|
withReplies,
|
||||||
}, {
|
}, {
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
|
@ -248,8 +249,8 @@ export class QueueService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createImportFollowingToDbJob(user: ThinUser, targets: string[]) {
|
public createImportFollowingToDbJob(user: ThinUser, targets: string[], withReplies?: boolean) {
|
||||||
const jobs = targets.map(rel => this.generateToDbJobData('importFollowingToDb', { user, target: rel }));
|
const jobs = targets.map(rel => this.generateToDbJobData('importFollowingToDb', { user, target: rel, withReplies }));
|
||||||
return this.dbQueue.addBulk(jobs);
|
return this.dbQueue.addBulk(jobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,7 +343,7 @@ export class QueueService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean }[]) {
|
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean, withReplies?: boolean }[]) {
|
||||||
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
|
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
|
||||||
return this.relationshipQueue.addBulk(jobs);
|
return this.relationshipQueue.addBulk(jobs);
|
||||||
}
|
}
|
||||||
|
@ -384,6 +385,7 @@ export class QueueService {
|
||||||
to: { id: data.to.id },
|
to: { id: data.to.id },
|
||||||
silent: data.silent,
|
silent: data.silent,
|
||||||
requestId: data.requestId,
|
requestId: data.requestId,
|
||||||
|
withReplies: data.withReplies,
|
||||||
},
|
},
|
||||||
opts: {
|
opts: {
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
|
|
|
@ -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': '👍',
|
||||||
|
@ -148,7 +149,7 @@ export class ReactionService {
|
||||||
reaction = FALLBACK;
|
reaction = FALLBACK;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
reaction = this.normalize(reaction ?? null);
|
reaction = this.normalize(reaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
export type RolePolicies = {
|
export type RolePolicies = {
|
||||||
|
@ -105,7 +105,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
private redisTimelineService: RedisTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
//this.onMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
|
@ -473,7 +473,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
const redisPipeline = this.redisClient.pipeline();
|
const redisPipeline = this.redisClient.pipeline();
|
||||||
|
|
||||||
for (const role of roles) {
|
for (const role of roles) {
|
||||||
this.redisTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline);
|
this.funoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline);
|
||||||
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
|
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
|
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
|
@ -28,6 +28,8 @@ import { MetaService } from '@/core/MetaService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import Logger from '../logger.js';
|
import Logger from '../logger.js';
|
||||||
|
|
||||||
const logger = new Logger('following/create');
|
const logger = new Logger('following/create');
|
||||||
|
@ -71,6 +73,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
|
private utilityService: UtilityService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
|
@ -81,6 +84,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
private webhookService: WebhookService,
|
private webhookService: WebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private accountMoveService: AccountMoveService,
|
private accountMoveService: AccountMoveService,
|
||||||
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
private perUserFollowingChart: PerUserFollowingChart,
|
private perUserFollowingChart: PerUserFollowingChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
) {
|
) {
|
||||||
|
@ -91,7 +95,15 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async follow(_follower: { id: MiUser['id'] }, _followee: { id: MiUser['id'] }, requestId?: string, silent = false): Promise<void> {
|
public async follow(
|
||||||
|
_follower: { id: MiUser['id'] },
|
||||||
|
_followee: { id: MiUser['id'] },
|
||||||
|
{ requestId, silent = false, withReplies }: {
|
||||||
|
requestId?: string,
|
||||||
|
silent?: boolean,
|
||||||
|
withReplies?: boolean,
|
||||||
|
} = {},
|
||||||
|
): Promise<void> {
|
||||||
const [follower, followee] = await Promise.all([
|
const [follower, followee] = await Promise.all([
|
||||||
this.usersRepository.findOneByOrFail({ id: _follower.id }),
|
this.usersRepository.findOneByOrFail({ id: _follower.id }),
|
||||||
this.usersRepository.findOneByOrFail({ id: _followee.id }),
|
this.usersRepository.findOneByOrFail({ id: _followee.id }),
|
||||||
|
@ -118,15 +130,16 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id });
|
const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id });
|
||||||
|
|
||||||
// フォロー対象が鍵アカウントである or
|
// フォロー対象が鍵アカウントである or
|
||||||
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
|
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
|
||||||
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
|
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or
|
||||||
|
// フォロワーがローカルユーザーであり、フォロー対象がサイレンスされているサーバーである
|
||||||
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
|
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
|
||||||
if (
|
if (
|
||||||
followee.isLocked ||
|
followee.isLocked ||
|
||||||
(followeeProfile.carefulBot && follower.isBot) ||
|
(followeeProfile.carefulBot && follower.isBot) ||
|
||||||
(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true')
|
(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') ||
|
||||||
|
(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost((await this.metaService.fetch()).silencedHosts, follower.host))
|
||||||
) {
|
) {
|
||||||
let autoAccept = false;
|
let autoAccept = false;
|
||||||
|
|
||||||
|
@ -168,12 +181,12 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!autoAccept) {
|
if (!autoAccept) {
|
||||||
await this.createFollowRequest(follower, followee, requestId);
|
await this.createFollowRequest(follower, followee, requestId, withReplies);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.insertFollowingDoc(followee, follower, silent);
|
await this.insertFollowingDoc(followee, follower, silent, withReplies);
|
||||||
|
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||||||
|
@ -190,6 +203,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']
|
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']
|
||||||
},
|
},
|
||||||
silent = false,
|
silent = false,
|
||||||
|
withReplies?: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (follower.id === followee.id) return;
|
if (follower.id === followee.id) return;
|
||||||
|
|
||||||
|
@ -199,6 +213,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
|
withReplies: withReplies,
|
||||||
|
|
||||||
// 非正規化
|
// 非正規化
|
||||||
followerHost: follower.host,
|
followerHost: follower.host,
|
||||||
|
@ -275,8 +290,8 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
this.perUserFollowingChart.update(follower, followee, true);
|
this.perUserFollowingChart.update(follower, followee, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish follow event
|
|
||||||
if (this.userEntityService.isLocalUser(follower) && !silent) {
|
if (this.userEntityService.isLocalUser(follower) && !silent) {
|
||||||
|
// Publish follow event
|
||||||
this.userEntityService.pack(followee.id, follower, {
|
this.userEntityService.pack(followee.id, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
|
@ -289,6 +304,8 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.funoutTimelineService.purge(`homeTimeline:${follower.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish followed event
|
// Publish followed event
|
||||||
|
@ -342,8 +359,8 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
this.decrementFollowing(following.follower, following.followee);
|
this.decrementFollowing(following.follower, following.followee);
|
||||||
|
|
||||||
// Publish unfollow event
|
|
||||||
if (!silent && this.userEntityService.isLocalUser(follower)) {
|
if (!silent && this.userEntityService.isLocalUser(follower)) {
|
||||||
|
// Publish unfollow event
|
||||||
this.userEntityService.pack(followee.id, follower, {
|
this.userEntityService.pack(followee.id, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
|
@ -356,6 +373,8 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.funoutTimelineService.purge(`homeTimeline:${follower.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
|
@ -451,6 +470,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'];
|
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'];
|
||||||
},
|
},
|
||||||
requestId?: string,
|
requestId?: string,
|
||||||
|
withReplies?: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (follower.id === followee.id) return;
|
if (follower.id === followee.id) return;
|
||||||
|
|
||||||
|
@ -468,6 +488,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
requestId,
|
requestId,
|
||||||
|
withReplies,
|
||||||
|
|
||||||
// 非正規化
|
// 非正規化
|
||||||
followerHost: follower.host,
|
followerHost: follower.host,
|
||||||
|
@ -552,7 +573,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.');
|
throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.insertFollowingDoc(followee, follower);
|
await this.insertFollowingDoc(followee, follower, false, request.withReplies);
|
||||||
|
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as MiPartialLocalUser, request.requestId!), followee));
|
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as MiPartialLocalUser, request.requestId!), followee));
|
||||||
|
@ -692,4 +713,12 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getFollowees(userId: MiUser['id']) {
|
||||||
|
return this.followingsRepository.createQueryBuilder('following')
|
||||||
|
.select('following.followeeId')
|
||||||
|
.where('following.followerId = :followerId', { followerId: userId })
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,12 @@ export class UtilityService {
|
||||||
return blockedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
return blockedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public isSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean {
|
||||||
|
if (!silencedHosts || host == null) return false;
|
||||||
|
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public extractDbHost(uri: string): string {
|
public extractDbHost(uri: string): string {
|
||||||
const url = new URL(uri);
|
const url = new URL(uri);
|
||||||
|
|
|
@ -164,7 +164,7 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't queue because the sender may attempt again when timeout
|
// don't queue because the sender may attempt again when timeout
|
||||||
await this.userFollowingService.follow(actor, followee, activity.id);
|
await this.userFollowingService.follow(actor, followee, { requestId: activity.id });
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ export class DriveFileEntityService {
|
||||||
if (file.type.startsWith('video')) {
|
if (file.type.startsWith('video')) {
|
||||||
if (file.thumbnailUrl) return file.thumbnailUrl;
|
if (file.thumbnailUrl) return file.thumbnailUrl;
|
||||||
|
|
||||||
return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url ?? file.uri);
|
return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url);
|
||||||
} else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
} else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
||||||
// 動画ではなくリモートかつメディアプロキシ
|
// 動画ではなくリモートかつメディアプロキシ
|
||||||
return this.getProxiedUrl(file.uri, 'static');
|
return this.getProxiedUrl(file.uri, 'static');
|
||||||
|
@ -145,7 +145,7 @@ export class DriveFileEntityService {
|
||||||
.select('SUM(file.size)', 'sum')
|
.select('SUM(file.size)', 'sum')
|
||||||
.getRawOne();
|
.getRawOne();
|
||||||
|
|
||||||
return parseInt(sum, 10) ?? 0;
|
return parseInt(sum, 10) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -157,7 +157,7 @@ export class DriveFileEntityService {
|
||||||
.select('SUM(file.size)', 'sum')
|
.select('SUM(file.size)', 'sum')
|
||||||
.getRawOne();
|
.getRawOne();
|
||||||
|
|
||||||
return parseInt(sum, 10) ?? 0;
|
return parseInt(sum, 10) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -169,7 +169,7 @@ export class DriveFileEntityService {
|
||||||
.select('SUM(file.size)', 'sum')
|
.select('SUM(file.size)', 'sum')
|
||||||
.getRawOne();
|
.getRawOne();
|
||||||
|
|
||||||
return parseInt(sum, 10) ?? 0;
|
return parseInt(sum, 10) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -181,7 +181,7 @@ export class DriveFileEntityService {
|
||||||
.select('SUM(file.size)', 'sum')
|
.select('SUM(file.size)', 'sum')
|
||||||
.getRawOne();
|
.getRawOne();
|
||||||
|
|
||||||
return parseInt(sum, 10) ?? 0;
|
return parseInt(sum, 10) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -3,9 +3,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { } from '@/models/Blocking.js';
|
|
||||||
import type { MiInstance } from '@/models/Instance.js';
|
import type { MiInstance } from '@/models/Instance.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
@ -43,6 +42,7 @@ export class InstanceEntityService {
|
||||||
description: instance.description,
|
description: instance.description,
|
||||||
maintainerName: instance.maintainerName,
|
maintainerName: instance.maintainerName,
|
||||||
maintainerEmail: instance.maintainerEmail,
|
maintainerEmail: instance.maintainerEmail,
|
||||||
|
isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host),
|
||||||
iconUrl: instance.iconUrl,
|
iconUrl: instance.iconUrl,
|
||||||
faviconUrl: instance.faviconUrl,
|
faviconUrl: instance.faviconUrl,
|
||||||
themeColor: instance.themeColor,
|
themeColor: instance.themeColor,
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -318,13 +330,14 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
text: text,
|
text: text,
|
||||||
cw: note.cw,
|
cw: note.cw,
|
||||||
visibility: note.visibility,
|
visibility: note.visibility,
|
||||||
localOnly: note.localOnly ?? undefined,
|
localOnly: note.localOnly,
|
||||||
reactionAcceptance: note.reactionAcceptance,
|
reactionAcceptance: note.reactionAcceptance,
|
||||||
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
|
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
|
||||||
renoteCount: note.renoteCount,
|
renoteCount: note.renoteCount,
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -333,8 +333,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
host: user.host,
|
host: user.host,
|
||||||
avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user),
|
avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user),
|
||||||
avatarBlurhash: user.avatarBlurhash,
|
avatarBlurhash: user.avatarBlurhash,
|
||||||
isBot: user.isBot ?? falsy,
|
isBot: user.isBot,
|
||||||
isCat: user.isCat ?? falsy,
|
isCat: user.isCat,
|
||||||
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
||||||
name: instance.name,
|
name: instance.name,
|
||||||
softwareName: instance.softwareName,
|
softwareName: instance.softwareName,
|
||||||
|
@ -367,7 +367,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
bannerBlurhash: user.bannerBlurhash,
|
bannerBlurhash: user.bannerBlurhash,
|
||||||
isLocked: user.isLocked,
|
isLocked: user.isLocked,
|
||||||
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
||||||
isSuspended: user.isSuspended ?? falsy,
|
isSuspended: user.isSuspended,
|
||||||
description: profile!.description,
|
description: profile!.description,
|
||||||
location: profile!.location,
|
location: profile!.location,
|
||||||
birthday: profile!.birthday,
|
birthday: profile!.birthday,
|
||||||
|
|
|
@ -108,6 +108,5 @@ async function net() {
|
||||||
|
|
||||||
// FS STAT
|
// FS STAT
|
||||||
async function fs() {
|
async function fs() {
|
||||||
const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
|
return await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
|
||||||
return data ?? { rIO_sec: 0, wIO_sec: 0 };
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,11 @@ export class MiFollowRequest {
|
||||||
})
|
})
|
||||||
public requestId: string | null;
|
public requestId: string | null;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public withReplies: boolean;
|
||||||
|
|
||||||
//#region Denormalized fields
|
//#region Denormalized fields
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 128, nullable: true,
|
length: 128, nullable: true,
|
||||||
|
|
|
@ -76,6 +76,11 @@ export class MiMeta {
|
||||||
})
|
})
|
||||||
public sensitiveWords: string[];
|
public sensitiveWords: string[];
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024, array: true, default: '{}',
|
||||||
|
})
|
||||||
|
public silencedHosts: string[];
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 1024,
|
length: 1024,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
|
|
@ -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: '{}',
|
||||||
})
|
})
|
||||||
|
|
|
@ -93,6 +93,11 @@ export const packedFederationInstanceSchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
isSilenced: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
infoUpdatedAt: {
|
infoUpdatedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -56,7 +56,7 @@ export class ImportFollowingProcessorService {
|
||||||
|
|
||||||
const csv = await this.downloadService.downloadTextFile(file.url);
|
const csv = await this.downloadService.downloadTextFile(file.url);
|
||||||
const targets = csv.trim().split('\n');
|
const targets = csv.trim().split('\n');
|
||||||
this.queueService.createImportFollowingToDbJob({ id: user.id }, targets);
|
this.queueService.createImportFollowingToDbJob({ id: user.id }, targets, job.data.withReplies);
|
||||||
|
|
||||||
this.logger.succ('Import jobs created');
|
this.logger.succ('Import jobs created');
|
||||||
}
|
}
|
||||||
|
@ -93,9 +93,9 @@ export class ImportFollowingProcessorService {
|
||||||
// skip myself
|
// skip myself
|
||||||
if (target.id === job.data.user.id) return;
|
if (target.id === job.data.user.id) return;
|
||||||
|
|
||||||
this.logger.info(`Follow ${target.id} ...`);
|
this.logger.info(`Follow ${target.id} ${job.data.withReplies ? 'with replies' : 'without replies'} ...`);
|
||||||
|
|
||||||
this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true }]);
|
this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: job.data.withReplies }]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.warn(`Error: ${e}`);
|
this.logger.warn(`Error: ${e}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@ export class InboxProcessorService {
|
||||||
if (err.isClientError) {
|
if (err.isClientError) {
|
||||||
throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
|
throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
|
||||||
}
|
}
|
||||||
throw new Error(`Error in actor ${activity.actor} - ${err.statusCode ?? err}`);
|
throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,12 @@ export class RelationshipProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async processFollow(job: Bull.Job<RelationshipJobData>): Promise<string> {
|
public async processFollow(job: Bull.Job<RelationshipJobData>): Promise<string> {
|
||||||
this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id}`);
|
this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id} ${job.data.withReplies ? "with replies" : "without replies"}`);
|
||||||
await this.userFollowingService.follow(job.data.from, job.data.to, job.data.requestId, job.data.silent);
|
await this.userFollowingService.follow(job.data.from, job.data.to, {
|
||||||
|
requestId: job.data.requestId,
|
||||||
|
silent: job.data.silent,
|
||||||
|
withReplies: job.data.withReplies,
|
||||||
|
});
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ export type RelationshipJobData = {
|
||||||
to: ThinUser;
|
to: ThinUser;
|
||||||
silent?: boolean;
|
silent?: boolean;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
|
withReplies?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DbJobData<T extends keyof DbJobMap> = DbJobMap[T];
|
export type DbJobData<T extends keyof DbJobMap> = DbJobMap[T];
|
||||||
|
@ -79,6 +80,7 @@ export type DbUserDeleteJobData = {
|
||||||
export type DbUserImportJobData = {
|
export type DbUserImportJobData = {
|
||||||
user: ThinUser;
|
user: ThinUser;
|
||||||
fileId: MiDriveFile['id'];
|
fileId: MiDriveFile['id'];
|
||||||
|
withReplies?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DBAntennaImportJobData = {
|
export type DBAntennaImportJobData = {
|
||||||
|
@ -89,6 +91,7 @@ export type DBAntennaImportJobData = {
|
||||||
export type DbUserImportToDbJobData = {
|
export type DbUserImportToDbJobData = {
|
||||||
user: ThinUser;
|
user: ThinUser;
|
||||||
target: string;
|
target: string;
|
||||||
|
withReplies?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ObjectStorageJobData = ObjectStorageFileJobData | Record<string, unknown>;
|
export type ObjectStorageJobData = ObjectStorageFileJobData | Record<string, unknown>;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -41,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
return ips.map(x => ({
|
return ips.map(x => ({
|
||||||
ip: x.ip,
|
ip: x.ip,
|
||||||
createdAt: this.idService.parse(x.id).date.toISOString(),
|
createdAt: x.createdAt.toISOString(),
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,16 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
silencedHosts: {
|
||||||
|
type: "array",
|
||||||
|
optional: true,
|
||||||
|
nullable: false,
|
||||||
|
items: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
pinnedUsers: {
|
pinnedUsers: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -367,6 +377,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
pinnedUsers: instance.pinnedUsers,
|
pinnedUsers: instance.pinnedUsers,
|
||||||
hiddenTags: instance.hiddenTags,
|
hiddenTags: instance.hiddenTags,
|
||||||
blockedHosts: instance.blockedHosts,
|
blockedHosts: instance.blockedHosts,
|
||||||
|
silencedHosts: instance.silencedHosts,
|
||||||
sensitiveWords: instance.sensitiveWords,
|
sensitiveWords: instance.sensitiveWords,
|
||||||
preservedUsernames: instance.preservedUsernames,
|
preservedUsernames: instance.preservedUsernames,
|
||||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||||
|
|
|
@ -20,18 +20,26 @@ export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
disableRegistration: { type: 'boolean', nullable: true },
|
disableRegistration: { type: 'boolean', nullable: true },
|
||||||
pinnedUsers: { type: 'array', nullable: true, items: {
|
pinnedUsers: {
|
||||||
type: 'string',
|
type: 'array', nullable: true, items: {
|
||||||
} },
|
type: 'string',
|
||||||
hiddenTags: { type: 'array', nullable: true, items: {
|
},
|
||||||
type: 'string',
|
},
|
||||||
} },
|
hiddenTags: {
|
||||||
blockedHosts: { type: 'array', nullable: true, items: {
|
type: 'array', nullable: true, items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
},
|
||||||
sensitiveWords: { type: 'array', nullable: true, items: {
|
},
|
||||||
type: 'string',
|
blockedHosts: {
|
||||||
} },
|
type: 'array', nullable: true, items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sensitiveWords: {
|
||||||
|
type: 'array', nullable: true, items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
|
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
|
||||||
mascotImageUrl: { type: 'string', nullable: true },
|
mascotImageUrl: { type: 'string', nullable: true },
|
||||||
bannerUrl: { type: 'string', nullable: true },
|
bannerUrl: { type: 'string', nullable: true },
|
||||||
|
@ -67,9 +75,11 @@ export const paramDef = {
|
||||||
proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true },
|
proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
maintainerName: { type: 'string', nullable: true },
|
maintainerName: { type: 'string', nullable: true },
|
||||||
maintainerEmail: { type: 'string', nullable: true },
|
maintainerEmail: { type: 'string', nullable: true },
|
||||||
langs: { type: 'array', items: {
|
langs: {
|
||||||
type: 'string',
|
type: 'array', items: {
|
||||||
} },
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
summalyProxy: { type: 'string', nullable: true },
|
summalyProxy: { type: 'string', nullable: true },
|
||||||
deeplAuthKey: { type: 'string', nullable: true },
|
deeplAuthKey: { type: 'string', nullable: true },
|
||||||
deeplIsPro: { type: 'boolean' },
|
deeplIsPro: { type: 'boolean' },
|
||||||
|
@ -86,8 +96,8 @@ export const paramDef = {
|
||||||
tosUrl: { type: 'string', nullable: true },
|
tosUrl: { type: 'string', nullable: true },
|
||||||
repositoryUrl: { type: 'string' },
|
repositoryUrl: { type: 'string' },
|
||||||
feedbackUrl: { type: 'string' },
|
feedbackUrl: { type: 'string' },
|
||||||
impressumUrl: { type: 'string' },
|
impressumUrl: { type: 'string', nullable: true },
|
||||||
privacyPolicyUrl: { type: 'string' },
|
privacyPolicyUrl: { type: 'string', nullable: true },
|
||||||
useObjectStorage: { type: 'boolean' },
|
useObjectStorage: { type: 'boolean' },
|
||||||
objectStorageBaseUrl: { type: 'string', nullable: true },
|
objectStorageBaseUrl: { type: 'string', nullable: true },
|
||||||
objectStorageBucket: { type: 'string', nullable: true },
|
objectStorageBucket: { type: 'string', nullable: true },
|
||||||
|
@ -115,6 +125,13 @@ export const paramDef = {
|
||||||
perUserHomeTimelineCacheMax: { type: 'integer' },
|
perUserHomeTimelineCacheMax: { type: 'integer' },
|
||||||
perUserListTimelineCacheMax: { type: 'integer' },
|
perUserListTimelineCacheMax: { type: 'integer' },
|
||||||
notesPerOneAd: { type: 'integer' },
|
notesPerOneAd: { type: 'integer' },
|
||||||
|
silencedHosts: {
|
||||||
|
type: 'array',
|
||||||
|
nullable: true,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -147,7 +164,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (Array.isArray(ps.sensitiveWords)) {
|
if (Array.isArray(ps.sensitiveWords)) {
|
||||||
set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
|
set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(ps.silencedHosts)) {
|
||||||
|
let lastValue = '';
|
||||||
|
set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
|
||||||
|
const lv = lastValue;
|
||||||
|
lastValue = h;
|
||||||
|
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
|
||||||
|
});
|
||||||
|
}
|
||||||
if (ps.themeColor !== undefined) {
|
if (ps.themeColor !== undefined) {
|
||||||
set.themeColor = ps.themeColor;
|
set.themeColor = ps.themeColor;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { NoteReadService } from '@/core/NoteReadService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private noteReadService: NoteReadService,
|
private noteReadService: NoteReadService,
|
||||||
private redisTimelineService: RedisTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||||
|
@ -90,7 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
lastUsedAt: new Date(),
|
lastUsedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let noteIds = await this.redisTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId);
|
let noteIds = await this.funoutTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId);
|
||||||
noteIds = noteIds.slice(0, ps.limit);
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
if (noteIds.length === 0) {
|
if (noteIds.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
@ -69,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private redisTimelineService: RedisTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
) {
|
) {
|
||||||
|
@ -95,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
]) : [new Set<string>()];
|
]) : [new Set<string>()];
|
||||||
|
|
||||||
let noteIds = await this.redisTimelineService.get(`channelTimeline:${channel.id}`, untilId, sinceId);
|
let noteIds = await this.funoutTimelineService.get(`channelTimeline:${channel.id}`, untilId, sinceId);
|
||||||
noteIds = noteIds.slice(0, ps.limit);
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
if (noteIds.length > 0) {
|
if (noteIds.length > 0) {
|
||||||
|
|
|
@ -36,6 +36,7 @@ export const paramDef = {
|
||||||
blocked: { type: 'boolean', nullable: true },
|
blocked: { type: 'boolean', nullable: true },
|
||||||
notResponding: { type: 'boolean', nullable: true },
|
notResponding: { type: 'boolean', nullable: true },
|
||||||
suspended: { type: 'boolean', nullable: true },
|
suspended: { type: 'boolean', nullable: true },
|
||||||
|
silenced: { type: "boolean", nullable: true },
|
||||||
federating: { type: 'boolean', nullable: true },
|
federating: { type: 'boolean', nullable: true },
|
||||||
subscribing: { type: 'boolean', nullable: true },
|
subscribing: { type: 'boolean', nullable: true },
|
||||||
publishing: { type: 'boolean', nullable: true },
|
publishing: { type: 'boolean', nullable: true },
|
||||||
|
@ -102,6 +103,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof ps.silenced === "boolean") {
|
||||||
|
const meta = await this.metaService.fetch(true);
|
||||||
|
|
||||||
|
if (ps.silenced) {
|
||||||
|
if (meta.silencedHosts.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
query.andWhere("instance.host IN (:...silences)", {
|
||||||
|
silences: meta.silencedHosts,
|
||||||
|
});
|
||||||
|
} else if (meta.silencedHosts.length > 0) {
|
||||||
|
query.andWhere("instance.host NOT IN (:...silences)", {
|
||||||
|
silences: meta.silencedHosts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof ps.federating === 'boolean') {
|
if (typeof ps.federating === 'boolean') {
|
||||||
if (ps.federating) {
|
if (ps.federating) {
|
||||||
query.andWhere('((instance.followingCount > 0) OR (instance.followersCount > 0))');
|
query.andWhere('((instance.followingCount > 0) OR (instance.followersCount > 0))');
|
||||||
|
|
|
@ -71,6 +71,7 @@ export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
userId: { type: 'string', format: 'misskey:id' },
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
withReplies: { type: 'boolean' }
|
||||||
},
|
},
|
||||||
required: ['userId'],
|
required: ['userId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -112,7 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.userFollowingService.follow(follower, followee);
|
await this.userFollowingService.follow(follower, followee, { withReplies: ps.withReplies });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof IdentifiableError) {
|
if (e instanceof IdentifiableError) {
|
||||||
if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking);
|
if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking);
|
||||||
|
|
|
@ -52,6 +52,7 @@ export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
fileId: { type: 'string', format: 'misskey:id' },
|
fileId: { type: 'string', format: 'misskey:id' },
|
||||||
|
withReplies: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
required: ['fileId'],
|
required: ['fileId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -79,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
);
|
);
|
||||||
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
||||||
|
|
||||||
this.queueService.createImportFollowingJob(me, file.id);
|
this.queueService.createImportFollowingJob(me, file.id, ps.withReplies);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
|
||||||
import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js';
|
import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
|
@ -15,7 +14,9 @@ import { RoleService } from '@/core/RoleService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -63,9 +64,6 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisForTimelines)
|
|
||||||
private redisForTimelines: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@ -74,7 +72,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private redisTimelineService: RedisTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
|
private queryService: QueryService,
|
||||||
|
private userFollowingService: UserFollowingService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||||
|
@ -96,71 +96,152 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let noteIds: string[];
|
let noteIds: string[];
|
||||||
|
let shouldFallbackToDb = false;
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
const [htlNoteIds, ltlNoteIds] = await this.redisTimelineService.getMulti([
|
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||||
`homeTimelineWithFiles:${me.id}`,
|
`homeTimelineWithFiles:${me.id}`,
|
||||||
'localTimelineWithFiles',
|
'localTimelineWithFiles',
|
||||||
], untilId, sinceId);
|
], untilId, sinceId);
|
||||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||||
} else if (ps.withReplies) {
|
} else if (ps.withReplies) {
|
||||||
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.redisTimelineService.getMulti([
|
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||||
`homeTimeline:${me.id}`,
|
`homeTimeline:${me.id}`,
|
||||||
'localTimeline',
|
'localTimeline',
|
||||||
'localTimelineWithReplies',
|
'localTimelineWithReplies',
|
||||||
], untilId, sinceId);
|
], untilId, sinceId);
|
||||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
|
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
|
||||||
} else {
|
} else {
|
||||||
const [htlNoteIds, ltlNoteIds] = await this.redisTimelineService.getMulti([
|
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||||
`homeTimeline:${me.id}`,
|
`homeTimeline:${me.id}`,
|
||||||
'localTimeline',
|
'localTimeline',
|
||||||
], untilId, sinceId);
|
], untilId, sinceId);
|
||||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||||
|
shouldFallbackToDb = htlNoteIds.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
if (noteIds.length === 0) {
|
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = this.notesRepository.createQueryBuilder('note')
|
if (!shouldFallbackToDb) {
|
||||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
.leftJoinAndSelect('note.reply', 'reply')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
.leftJoinAndSelect('note.renote', 'renote')
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
.leftJoinAndSelect('note.channel', 'channel');
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
let timeline = await query.getMany();
|
let timeline = await query.getMany();
|
||||||
|
|
||||||
timeline = timeline.filter(note => {
|
timeline = timeline.filter(note => {
|
||||||
if (note.userId === me.id) {
|
if (note.userId === me.id) {
|
||||||
return true;
|
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 (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;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||||
|
|
||||||
|
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
|
||||||
|
process.nextTick(() => {
|
||||||
|
this.activeUsersChart.read(me);
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
|
} else { // fallback to db
|
||||||
|
const followees = await this.userFollowingService.getFollowees(me.id);
|
||||||
|
|
||||||
|
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
|
.andWhere(new Brackets(qb => {
|
||||||
|
if (followees.length > 0) {
|
||||||
|
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
|
||||||
|
qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
|
||||||
|
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
|
||||||
|
} else {
|
||||||
|
qb.where('note.userId = :meId', { meId: me.id });
|
||||||
|
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
|
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');
|
||||||
|
}));
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
});
|
this.queryService.generateMutedUserQuery(query, me);
|
||||||
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
||||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
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)');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
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)');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
process.nextTick(() => {
|
if (ps.includeLocalRenotes === false) {
|
||||||
this.activeUsersChart.read(me);
|
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)');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return await this.noteEntityService.packMany(timeline, me);
|
if (ps.withFiles) {
|
||||||
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
const timeline = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
process.nextTick(() => {
|
||||||
|
this.activeUsersChart.read(me);
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
|
||||||
import type { MiNote, NotesRepository } from '@/models/_.js';
|
import type { MiNote, NotesRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
|
@ -15,7 +14,8 @@ import { RoleService } from '@/core/RoleService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -59,9 +59,6 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisForTimelines)
|
|
||||||
private redisForTimelines: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@ -70,7 +67,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private redisTimelineService: RedisTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
|
private queryService: QueryService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||||
|
@ -94,9 +92,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
let noteIds: string[];
|
let noteIds: string[];
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
noteIds = await this.redisTimelineService.get('localTimelineWithFiles', untilId, sinceId);
|
noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
|
||||||
} else {
|
} else {
|
||||||
const [nonReplyNoteIds, replyNoteIds] = await this.redisTimelineService.getMulti([
|
const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||||
'localTimeline',
|
'localTimeline',
|
||||||
'localTimelineWithReplies',
|
'localTimelineWithReplies',
|
||||||
], untilId, sinceId);
|
], untilId, sinceId);
|
||||||
|
@ -106,49 +104,87 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
noteIds = noteIds.slice(0, ps.limit);
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
if (noteIds.length === 0) {
|
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 query = this.notesRepository.createQueryBuilder('note')
|
let timeline = await query.getMany();
|
||||||
.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 && (note.userId === me.id)) {
|
||||||
timeline = timeline.filter(note => {
|
return true;
|
||||||
if (me && (note.userId === me.id)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!ps.withReplies && note.replyId && (me == null || note.replyUserId !== me.id)) return false;
|
|
||||||
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.withReplies && note.replyId && (me == null || note.replyUserId !== me.id)) return false;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||||
|
|
||||||
|
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
|
||||||
|
process.nextTick(() => {
|
||||||
|
if (me) {
|
||||||
|
this.activeUsersChart.read(me);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
|
} else { // fallback to db
|
||||||
|
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||||
|
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
|
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
|
||||||
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
|
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||||
|
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
||||||
|
if (ps.withFiles) {
|
||||||
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
if (!ps.withReplies) {
|
||||||
});
|
query.andWhere(new Brackets(qb => {
|
||||||
|
qb
|
||||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
.where('note.replyId IS NULL') // 返信ではない
|
||||||
|
.orWhere(new Brackets(qb => {
|
||||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
qb // 返信だけど投稿者自身への返信
|
||||||
|
.where('note.replyId IS NOT NULL')
|
||||||
process.nextTick(() => {
|
.andWhere('note.replyUserId = note.userId');
|
||||||
if (me) {
|
}));
|
||||||
this.activeUsersChart.read(me);
|
}));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return await this.noteEntityService.packMany(timeline, me);
|
const timeline = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
process.nextTick(() => {
|
||||||
|
if (me) {
|
||||||
|
this.activeUsersChart.read(me);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,7 @@
|
||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import type { NotesRepository } from '@/models/_.js';
|
||||||
import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
|
@ -15,7 +14,8 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
|
@ -53,9 +53,6 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisForTimelines)
|
|
||||||
private redisForTimelines: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@ -63,7 +60,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private redisTimelineService: RedisTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
|
private userFollowingService: UserFollowingService,
|
||||||
|
private queryService: QueryService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||||
|
@ -81,52 +80,127 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
this.cacheService.userBlockedCache.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);
|
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
|
||||||
noteIds = noteIds.slice(0, ps.limit);
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
if (noteIds.length === 0) {
|
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 query = this.notesRepository.createQueryBuilder('note')
|
let timeline = await query.getMany();
|
||||||
.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 (note.userId === me.id) {
|
||||||
timeline = timeline.filter(note => {
|
return true;
|
||||||
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 (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||||
if (note.reply && note.reply.visibility === 'followers') {
|
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||||
if (!Object.hasOwn(followings, note.reply.userId)) 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;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||||
|
|
||||||
|
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
|
||||||
|
process.nextTick(() => {
|
||||||
|
this.activeUsersChart.read(me);
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
|
} else { // fallback to db
|
||||||
|
const followees = await this.userFollowingService.getFollowees(me.id);
|
||||||
|
|
||||||
|
//#region Construct query
|
||||||
|
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
|
.andWhere('note.channelId IS NULL')
|
||||||
|
.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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
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');
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
|
||||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
|
this.queryService.generateMutedUserQuery(query, me);
|
||||||
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
||||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
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)');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
process.nextTick(() => {
|
if (ps.includeRenotedMyNotes === false) {
|
||||||
this.activeUsersChart.read(me);
|
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)');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return await this.noteEntityService.packMany(timeline, me);
|
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 != \'{}\'');
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
const timeline = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
process.nextTick(() => {
|
||||||
|
this.activeUsersChart.read(me);
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private redisTimelineService: RedisTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||||
|
@ -105,7 +105,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
this.cacheService.userBlockedCache.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);
|
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId);
|
||||||
noteIds = noteIds.slice(0, ps.limit);
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
if (noteIds.length === 0) {
|
if (noteIds.length === 0) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { QueryService } from '@/core/QueryService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private redisTimelineService: RedisTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||||
|
@ -84,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
let noteIds = await this.redisTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId);
|
let noteIds = await this.funoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId);
|
||||||
noteIds = noteIds.slice(0, ps.limit);
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
if (noteIds.length === 0) {
|
if (noteIds.length === 0) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { CacheService } from '@/core/CacheService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private redisTimelineService: RedisTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||||
|
@ -87,9 +87,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
]) : [new Set<string>()];
|
]) : [new Set<string>()];
|
||||||
|
|
||||||
const [noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([
|
const [noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([
|
||||||
this.redisTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId),
|
this.funoutTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId),
|
||||||
ps.withReplies ? this.redisTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
|
ps.withReplies ? this.funoutTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
|
||||||
ps.withChannelNotes ? this.redisTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
|
ps.withChannelNotes ? this.funoutTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let noteIds = Array.from(new Set([
|
let noteIds = Array.from(new Set([
|
||||||
|
@ -151,7 +151,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
if (ps.withChannelNotes) {
|
if (ps.withChannelNotes) {
|
||||||
if (!isSelf) query.andWhere('channel.isSensitive = false');
|
if (!isSelf) query.andWhere(new Brackets(qb => {
|
||||||
|
qb.orWhere('note.channelId IS NULL');
|
||||||
|
qb.orWhere('channel.isSensitive = false');
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
query.andWhere('note.channelId IS NULL');
|
query.andWhere('note.channelId IS NULL');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -49,7 +49,7 @@ class HomeTimelineChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore notes from instances the user has muted
|
// Ignore notes from instances the user has muted
|
||||||
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
|
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances))) return;
|
||||||
|
|
||||||
if (note.visibility === 'followers') {
|
if (note.visibility === 'followers') {
|
||||||
if (!Object.hasOwn(this.following, note.userId)) return;
|
if (!Object.hasOwn(this.following, note.userId)) return;
|
||||||
|
@ -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);
|
||||||
|
|
|
@ -69,7 +69,7 @@ class HybridTimelineChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore notes from instances the user has muted
|
// Ignore notes from instances the user has muted
|
||||||
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
|
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances))) return;
|
||||||
|
|
||||||
// 関係ない返信は除外
|
// 関係ない返信は除外
|
||||||
if (note.reply && !this.following[note.userId]?.withReplies && !this.withReplies) {
|
if (note.reply && !this.following[note.userId]?.withReplies && !this.withReplies) {
|
||||||
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -83,7 +83,7 @@ export class FeedService {
|
||||||
date: this.idService.parse(note.id).date,
|
date: this.idService.parse(note.id).date,
|
||||||
description: note.cw ?? undefined,
|
description: note.cw ?? undefined,
|
||||||
content: note.text ?? undefined,
|
content: note.text ?? undefined,
|
||||||
image: file ? this.driveFileEntityService.getPublicUrl(file) ?? undefined : undefined,
|
image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -727,7 +727,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
await waitForPushToTl();
|
await waitForPushToTl();
|
||||||
|
|
||||||
const res = await api('/notes/hybrid-timeline', { }, alice);
|
const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
"@github/webauthn-json": "2.1.1",
|
"@github/webauthn-json": "2.1.1",
|
||||||
"@rollup/plugin-alias": "5.0.1",
|
"@rollup/plugin-alias": "5.0.1",
|
||||||
"@rollup/plugin-json": "6.0.1",
|
"@rollup/plugin-json": "6.0.1",
|
||||||
"@rollup/plugin-replace": "5.0.3",
|
"@rollup/plugin-replace": "5.0.4",
|
||||||
"@rollup/pluginutils": "5.0.5",
|
"@rollup/pluginutils": "5.0.5",
|
||||||
"@syuilo/aiscript": "0.16.0",
|
"@syuilo/aiscript": "0.16.0",
|
||||||
"@tabler/icons-webfont": "2.37.0",
|
"@tabler/icons-webfont": "2.37.0",
|
||||||
|
@ -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.3.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",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
"chartjs-chart-matrix": "2.0.1",
|
"chartjs-chart-matrix": "2.0.1",
|
||||||
"chartjs-plugin-gradient": "0.6.1",
|
"chartjs-plugin-gradient": "0.6.1",
|
||||||
"chartjs-plugin-zoom": "2.0.1",
|
"chartjs-plugin-zoom": "2.0.1",
|
||||||
"chromatic": "7.2.3",
|
"chromatic": "7.4.0",
|
||||||
"compare-versions": "6.1.0",
|
"compare-versions": "6.1.0",
|
||||||
"cropperjs": "2.0.0-beta.4",
|
"cropperjs": "2.0.0-beta.4",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
|
@ -57,9 +57,9 @@
|
||||||
"prismjs": "1.29.0",
|
"prismjs": "1.29.0",
|
||||||
"punycode": "2.3.0",
|
"punycode": "2.3.0",
|
||||||
"querystring": "0.2.1",
|
"querystring": "0.2.1",
|
||||||
"rollup": "4.0.2",
|
"rollup": "4.1.4",
|
||||||
"sanitize-html": "2.11.0",
|
"sanitize-html": "2.11.0",
|
||||||
"sass": "1.69.1",
|
"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,50 +72,50 @@
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-actions": "7.4.6",
|
"@storybook/addon-actions": "7.5.0",
|
||||||
"@storybook/addon-essentials": "7.4.6",
|
"@storybook/addon-essentials": "7.5.0",
|
||||||
"@storybook/addon-interactions": "7.4.6",
|
"@storybook/addon-interactions": "7.5.0",
|
||||||
"@storybook/addon-links": "7.4.6",
|
"@storybook/addon-links": "7.5.0",
|
||||||
"@storybook/addon-storysource": "7.4.6",
|
"@storybook/addon-storysource": "7.5.0",
|
||||||
"@storybook/addons": "7.4.6",
|
"@storybook/addons": "7.5.0",
|
||||||
"@storybook/blocks": "7.4.6",
|
"@storybook/blocks": "7.5.0",
|
||||||
"@storybook/core-events": "7.4.6",
|
"@storybook/core-events": "7.5.0",
|
||||||
"@storybook/jest": "0.2.3",
|
"@storybook/jest": "0.2.3",
|
||||||
"@storybook/manager-api": "7.4.6",
|
"@storybook/manager-api": "7.5.0",
|
||||||
"@storybook/preview-api": "7.4.6",
|
"@storybook/preview-api": "7.5.0",
|
||||||
"@storybook/react": "7.4.6",
|
"@storybook/react": "7.5.0",
|
||||||
"@storybook/react-vite": "7.4.6",
|
"@storybook/react-vite": "7.5.0",
|
||||||
"@storybook/testing-library": "0.2.2",
|
"@storybook/testing-library": "0.2.2",
|
||||||
"@storybook/theming": "7.4.6",
|
"@storybook/theming": "7.5.0",
|
||||||
"@storybook/types": "7.4.6",
|
"@storybook/types": "7.5.0",
|
||||||
"@storybook/vue3": "7.4.6",
|
"@storybook/vue3": "7.5.0",
|
||||||
"@storybook/vue3-vite": "7.4.6",
|
"@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.4",
|
"@types/node": "20.8.7",
|
||||||
"@types/punycode": "2.1.0",
|
"@types/punycode": "2.1.1",
|
||||||
"@types/sanitize-html": "2.9.1",
|
"@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.6",
|
"@types/ws": "8.5.8",
|
||||||
"@typescript-eslint/eslint-plugin": "6.7.5",
|
"@typescript-eslint/eslint-plugin": "6.8.0",
|
||||||
"@typescript-eslint/parser": "6.7.5",
|
"@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.0",
|
"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",
|
||||||
|
@ -123,19 +123,19 @@
|
||||||
"happy-dom": "10.0.3",
|
"happy-dom": "10.0.3",
|
||||||
"micromatch": "4.0.5",
|
"micromatch": "4.0.5",
|
||||||
"msw": "1.3.2",
|
"msw": "1.3.2",
|
||||||
"msw-storybook-addon": "1.8.0",
|
"msw-storybook-addon": "1.9.0",
|
||||||
"nodemon": "3.0.1",
|
"nodemon": "3.0.1",
|
||||||
"prettier": "3.0.3",
|
"prettier": "3.0.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"start-server-and-test": "2.0.1",
|
"start-server-and-test": "2.0.1",
|
||||||
"storybook": "7.4.6",
|
"storybook": "7.5.0",
|
||||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||||
"summaly": "github:misskey-dev/summaly",
|
"summaly": "github:misskey-dev/summaly",
|
||||||
"vite-plugin-turbosnap": "1.0.3",
|
"vite-plugin-turbosnap": "1.0.3",
|
||||||
"vitest": "0.34.6",
|
"vitest": "0.34.6",
|
||||||
"vitest-fetch-mock": "0.2.2",
|
"vitest-fetch-mock": "0.2.2",
|
||||||
"vue-eslint-parser": "9.3.2",
|
"vue-eslint-parser": "9.3.2",
|
||||||
"vue-tsc": "1.8.18"
|
"vue-tsc": "1.8.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ import { useStream } from '@/stream.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import { defaultStore } from "@/store.js";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
user: Misskey.entities.UserDetailed,
|
user: Misskey.entities.UserDetailed,
|
||||||
|
@ -52,6 +53,10 @@ const props = withDefaults(defineProps<{
|
||||||
large: false,
|
large: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(_: 'update:user', value: Misskey.entities.UserDetailed): void
|
||||||
|
}>();
|
||||||
|
|
||||||
let isFollowing = $ref(props.user.isFollowing);
|
let isFollowing = $ref(props.user.isFollowing);
|
||||||
let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
|
let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
|
||||||
let wait = $ref(false);
|
let wait = $ref(false);
|
||||||
|
@ -95,6 +100,11 @@ async function onClick() {
|
||||||
} else {
|
} else {
|
||||||
await os.api('following/create', {
|
await os.api('following/create', {
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
|
withReplies: defaultStore.state.defaultWithReplies,
|
||||||
|
});
|
||||||
|
emit('update:user', {
|
||||||
|
...props.user,
|
||||||
|
withReplies: defaultStore.state.defaultWithReplies
|
||||||
});
|
});
|
||||||
hasPendingFollowRequestFromYou = true;
|
hasPendingFollowRequestFromYou = true;
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.root, { yellow: instance.isNotResponding, red: instance.isBlocked, gray: instance.isSuspended }]">
|
<div :class="[$style.root, { yellow: instance.isNotResponding, red: instance.isBlocked, gray: instance.isSuspended, blue: instance.isSilenced }]">
|
||||||
<img class="icon" :src="getInstanceIcon(instance)" alt="" loading="lazy"/>
|
<img class="icon" :src="getInstanceIcon(instance)" alt="" loading="lazy"/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<span class="host">{{ instance.name ?? instance.host }}</span>
|
<span class="host">{{ instance.name ?? instance.host }}</span>
|
||||||
|
@ -89,6 +89,12 @@ function getInstanceIcon(instance): string {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:global(.blue) {
|
||||||
|
--c: rgba(0, 42, 255, 0.15);
|
||||||
|
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
&:global(.yellow) {
|
&:global(.yellow) {
|
||||||
--c: rgb(255 196 0 / 15%);
|
--c: rgb(255 196 0 / 15%);
|
||||||
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
|
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
|
||||||
|
|
|
@ -232,6 +232,7 @@ const keymap = {
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
rootEl: el,
|
rootEl: el,
|
||||||
note: $$(appearNote),
|
note: $$(appearNote),
|
||||||
|
pureNote: $$(note),
|
||||||
isDeletedRef: isDeleted,
|
isDeletedRef: isDeleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -296,6 +296,7 @@ const reactionsPagination = $computed(() => ({
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
rootEl: el,
|
rootEl: el,
|
||||||
note: $$(appearNote),
|
note: $$(appearNote),
|
||||||
|
pureNote: $$(note),
|
||||||
isDeletedRef: isDeleted,
|
isDeletedRef: isDeleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="_button" :class="$style.menu" @click="showMenu"><i class="ti ti-dots"></i></button>
|
<button class="_button" :class="$style.menu" @click="showMenu"><i class="ti ti-dots"></i></button>
|
||||||
<MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/>
|
<MkFollowButton v-if="$i && user.id != $i.id" v-model:user="user" :class="$style.follow" mini/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<MkLoading/>
|
<MkLoading/>
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -190,6 +190,9 @@ const patronsWithIcon = [{
|
||||||
}, {
|
}, {
|
||||||
name: '百日紅',
|
name: '百日紅',
|
||||||
icon: 'https://misskey-hub.net/patrons/302dce2898dd457ba03c3f7dc037900b.jpg',
|
icon: 'https://misskey-hub.net/patrons/302dce2898dd457ba03c3f7dc037900b.jpg',
|
||||||
|
}, {
|
||||||
|
name: 'taichan',
|
||||||
|
icon: 'https://misskey-hub.net/patrons/f981ab0159fb4e2c998e05f7263e1cd9.png',
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const patrons = [
|
const patrons = [
|
||||||
|
|
|
@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<option value="subscribing">{{ i18n.ts.subscribing }}</option>
|
<option value="subscribing">{{ i18n.ts.subscribing }}</option>
|
||||||
<option value="publishing">{{ i18n.ts.publishing }}</option>
|
<option value="publishing">{{ i18n.ts.publishing }}</option>
|
||||||
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
||||||
|
<option value="silenced">{{ i18n.ts.silence }}</option>
|
||||||
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
||||||
<option value="notResponding">{{ i18n.ts.notResponding }}</option>
|
<option value="notResponding">{{ i18n.ts.notResponding }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
@ -75,6 +76,7 @@ const pagination = {
|
||||||
state === 'publishing' ? { publishing: true } :
|
state === 'publishing' ? { publishing: true } :
|
||||||
state === 'suspended' ? { suspended: true } :
|
state === 'suspended' ? { suspended: true } :
|
||||||
state === 'blocked' ? { blocked: true } :
|
state === 'blocked' ? { blocked: true } :
|
||||||
|
state === 'silenced' ? { silenced: true } :
|
||||||
state === 'notResponding' ? { notResponding: true } :
|
state === 'notResponding' ? { notResponding: true } :
|
||||||
{}),
|
{}),
|
||||||
})),
|
})),
|
||||||
|
@ -83,6 +85,7 @@ const pagination = {
|
||||||
function getStatus(instance) {
|
function getStatus(instance) {
|
||||||
if (instance.isSuspended) return 'Suspended';
|
if (instance.isSuspended) return 'Suspended';
|
||||||
if (instance.isBlocked) return 'Blocked';
|
if (instance.isBlocked) return 'Blocked';
|
||||||
|
if (instance.isSilenced) return 'Silenced';
|
||||||
if (instance.isNotResponding) return 'Error';
|
if (instance.isNotResponding) return 'Error';
|
||||||
return 'Alive';
|
return 'Alive';
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||||
<FormSuspense :p="init">
|
<FormSuspense :p="init">
|
||||||
<MkTextarea v-model="blockedHosts">
|
<MkTextarea v-if="tab === 'block'" v-model="blockedHosts">
|
||||||
<span>{{ i18n.ts.blockedInstances }}</span>
|
<span>{{ i18n.ts.blockedInstances }}</span>
|
||||||
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
|
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
|
<MkTextarea v-else-if="tab === 'silence'" v-model="silencedHosts" class="_formBlock">
|
||||||
|
<span>{{ i18n.ts.silencedInstances }}</span>
|
||||||
|
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
|
||||||
|
</MkTextarea>
|
||||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
|
@ -20,7 +23,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
|
||||||
import XHeader from './_header_.vue';
|
import XHeader from './_header_.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
|
@ -31,15 +33,20 @@ import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
|
||||||
let blockedHosts: string = $ref('');
|
let blockedHosts: string = $ref('');
|
||||||
|
let silencedHosts: string = $ref('');
|
||||||
|
let tab = $ref('block');
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const meta = await os.api('admin/meta');
|
const meta = await os.api('admin/meta');
|
||||||
blockedHosts = meta.blockedHosts.join('\n');
|
blockedHosts = meta.blockedHosts.join('\n');
|
||||||
|
silencedHosts = meta.silencedHosts.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
blockedHosts: blockedHosts.split('\n') || [],
|
blockedHosts: blockedHosts.split('\n') || [],
|
||||||
|
silencedHosts: silencedHosts.split('\n') || [],
|
||||||
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
});
|
});
|
||||||
|
@ -47,7 +54,15 @@ function save() {
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
const headerActions = $computed(() => []);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => [{
|
||||||
|
key: 'block',
|
||||||
|
title: i18n.ts.block,
|
||||||
|
icon: 'ti ti-ban',
|
||||||
|
}, {
|
||||||
|
key: 'silence',
|
||||||
|
title: i18n.ts.silence,
|
||||||
|
icon: 'ti ti-eye-off',
|
||||||
|
}]);
|
||||||
|
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
title: i18n.ts.instanceBlocking,
|
title: i18n.ts.instanceBlocking,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { mainRouter } from '@/router.js';
|
import { mainRouter } from '@/router.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { defaultStore } from "@/store.js";
|
||||||
|
|
||||||
async function follow(user): Promise<void> {
|
async function follow(user): Promise<void> {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
|
@ -28,7 +29,9 @@ async function follow(user): Promise<void> {
|
||||||
|
|
||||||
os.apiWithDialog('following/create', {
|
os.apiWithDialog('following/create', {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
withReplies: defaultStore.state.defaultWithReplies,
|
||||||
});
|
});
|
||||||
|
user.withReplies = defaultStore.state.defaultWithReplies;
|
||||||
}
|
}
|
||||||
|
|
||||||
const acct = new URL(location.href).searchParams.get('acct');
|
const acct = new URL(location.href).searchParams.get('acct');
|
||||||
|
|
|
@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkUserName :user="post.user" style="display: block;"/>
|
<MkUserName :user="post.user" style="display: block;"/>
|
||||||
<MkAcct :user="post.user"/>
|
<MkAcct :user="post.user"/>
|
||||||
</div>
|
</div>
|
||||||
<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
<MkFollowButton v-if="!$i || $i.id != post.user.id" v-model:user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
||||||
|
|
|
@ -36,7 +36,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
|
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
|
||||||
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
||||||
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
|
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
||||||
|
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
@ -147,6 +148,7 @@ let meta = $ref<Misskey.entities.AdminInstanceMetadata | null>(null);
|
||||||
let instance = $ref<Misskey.entities.Instance | null>(null);
|
let instance = $ref<Misskey.entities.Instance | null>(null);
|
||||||
let suspended = $ref(false);
|
let suspended = $ref(false);
|
||||||
let isBlocked = $ref(false);
|
let isBlocked = $ref(false);
|
||||||
|
let isSilenced = $ref(false);
|
||||||
let faviconUrl = $ref<string | null>(null);
|
let faviconUrl = $ref<string | null>(null);
|
||||||
|
|
||||||
const usersPagination = {
|
const usersPagination = {
|
||||||
|
@ -169,7 +171,8 @@ async function fetch(): Promise<void> {
|
||||||
});
|
});
|
||||||
suspended = instance.isSuspended;
|
suspended = instance.isSuspended;
|
||||||
isBlocked = instance.isBlocked;
|
isBlocked = instance.isBlocked;
|
||||||
faviconUrl = getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.iconUrl, 'preview');
|
isSilenced = instance.isSilenced;
|
||||||
|
faviconUrl = getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.iconUrl, 'preview');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleBlock(): Promise<void> {
|
async function toggleBlock(): Promise<void> {
|
||||||
|
@ -180,7 +183,14 @@ async function toggleBlock(): Promise<void> {
|
||||||
blockedHosts: isBlocked ? meta.blockedHosts.concat([host]) : meta.blockedHosts.filter(x => x !== host),
|
blockedHosts: isBlocked ? meta.blockedHosts.concat([host]) : meta.blockedHosts.filter(x => x !== host),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
async function toggleSilenced(): Promise<void> {
|
||||||
|
if (!meta) throw new Error('No meta?');
|
||||||
|
if (!instance) throw new Error('No instance?');
|
||||||
|
const { host } = instance;
|
||||||
|
await os.api('admin/update-meta', {
|
||||||
|
silencedHosts: isSilenced ? meta.silencedHosts.concat([host]) : meta.silencedHosts.filter(x => x !== host),
|
||||||
|
});
|
||||||
|
}
|
||||||
async function toggleSuspend(): Promise<void> {
|
async function toggleSuspend(): Promise<void> {
|
||||||
if (!instance) throw new Error('No instance?');
|
if (!instance) throw new Error('No instance?');
|
||||||
await os.api('admin/federation/update-instance', {
|
await os.api('admin/federation/update-instance', {
|
||||||
|
|
|
@ -29,6 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
|
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
|
||||||
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
|
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch>
|
||||||
<MkFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.pinnedList }}</template>
|
<template #label>{{ i18n.ts.pinnedList }}</template>
|
||||||
<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
|
<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
|
||||||
|
@ -249,6 +250,7 @@ const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('
|
||||||
const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition'));
|
const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition'));
|
||||||
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
|
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
|
||||||
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
|
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
|
||||||
|
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
|
||||||
|
|
||||||
watch(lang, () => {
|
watch(lang, () => {
|
||||||
miLocalStorage.setItem('lang', lang.value as string);
|
miLocalStorage.setItem('lang', lang.value as string);
|
||||||
|
|
|
@ -40,6 +40,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkFolder v-if="$i && !$i.movedTo">
|
<MkFolder v-if="$i && !$i.movedTo">
|
||||||
<template #label>{{ i18n.ts.import }}</template>
|
<template #label>{{ i18n.ts.import }}</template>
|
||||||
<template #icon><i class="ti ti-upload"></i></template>
|
<template #icon><i class="ti ti-upload"></i></template>
|
||||||
|
<MkSwitch v-model="withReplies">
|
||||||
|
{{ i18n.ts._exportOrImport.withReplies }}
|
||||||
|
</MkSwitch>
|
||||||
<MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
|
@ -118,9 +121,11 @@ import { selectFile } from '@/scripts/select-file.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import { defaultStore } from "@/store.js";
|
||||||
|
|
||||||
const excludeMutingUsers = ref(false);
|
const excludeMutingUsers = ref(false);
|
||||||
const excludeInactiveUsers = ref(false);
|
const excludeInactiveUsers = ref(false);
|
||||||
|
const withReplies = ref(defaultStore.state.defaultWithReplies);
|
||||||
|
|
||||||
const onExportSuccess = () => {
|
const onExportSuccess = () => {
|
||||||
os.alert({
|
os.alert({
|
||||||
|
@ -177,7 +182,10 @@ const exportAntennas = () => {
|
||||||
|
|
||||||
const importFollowing = async (ev) => {
|
const importFollowing = async (ev) => {
|
||||||
const file = await selectFile(ev.currentTarget ?? ev.target);
|
const file = await selectFile(ev.currentTarget ?? ev.target);
|
||||||
os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError);
|
os.api('i/import-following', {
|
||||||
|
fileId: file.id,
|
||||||
|
withReplies: withReplies.value,
|
||||||
|
}).then(onImportSuccess).catch(onError);
|
||||||
};
|
};
|
||||||
|
|
||||||
const importUserLists = async (ev) => {
|
const importUserLists = async (ev) => {
|
||||||
|
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
|
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
|
||||||
<div v-if="$i" class="actions">
|
<div v-if="$i" class="actions">
|
||||||
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
|
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
|
||||||
<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
<MkFollowButton v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkAvatar class="avatar" :user="user" indicator/>
|
<MkAvatar class="avatar" :user="user" indicator/>
|
||||||
|
@ -198,6 +198,7 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
let user = $ref(props.user);
|
||||||
let parallaxAnimationId = $ref<null | number>(null);
|
let parallaxAnimationId = $ref<null | number>(null);
|
||||||
let narrow = $ref<null | boolean>(null);
|
let narrow = $ref<null | boolean>(null);
|
||||||
let rootEl = $ref<null | HTMLElement>(null);
|
let rootEl = $ref<null | HTMLElement>(null);
|
||||||
|
@ -232,7 +233,7 @@ const age = $computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function menu(ev) {
|
function menu(ev) {
|
||||||
const { menu, cleanup } = getUserMenu(props.user, router);
|
const { menu, cleanup } = getUserMenu(user, router);
|
||||||
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
|
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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, '냥');
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -361,6 +361,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
defaultWithReplies: {
|
||||||
|
where: 'account',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// TODO: 他のタブと永続化されたstateを同期
|
// TODO: 他のタブと永続化されたstateを同期
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue