Merge branch 'develop' into schedule-note
This commit is contained in:
commit
1a54663956
|
@ -8,7 +8,7 @@
|
||||||
"version": "8.9.2"
|
"version": "8.9.2"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"version": "20.5.1"
|
"version": "20.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"forwardPorts": [3000],
|
"forwardPorts": [3000],
|
||||||
|
|
|
@ -6,37 +6,33 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
|
paths:
|
||||||
|
- packages/backend/**
|
||||||
|
- .github/workflows/get-api-diff.yml
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
get-base:
|
get-from-misskey:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.5.1]
|
node-version: [20.10.0]
|
||||||
|
api-json-name: [api-base.json, api-head.json]
|
||||||
services:
|
include:
|
||||||
db:
|
- api-json-name: api-base.json
|
||||||
image: postgres:13
|
repo-name: ${{ github.event.pull_request.base.repo.full_name }}
|
||||||
ports:
|
ref: ${{ github.base_ref }}
|
||||||
- 5432:5432
|
- api-json-name: api-head.json
|
||||||
env:
|
repo-name: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
POSTGRES_DB: misskey
|
ref: ${{ github.head_ref }}
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
|
||||||
POSTGRES_USER: example-misskey-user
|
|
||||||
POSTGRESS_PASS: example-misskey-pass
|
|
||||||
redis:
|
|
||||||
image: redis:7
|
|
||||||
ports:
|
|
||||||
- 6379:6379
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
- uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
repository: ${{ matrix.repo-name }}
|
||||||
ref: ${{ github.base_ref }}
|
ref: ${{ matrix.ref }}
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
|
@ -56,121 +52,15 @@ jobs:
|
||||||
run: cp .config/example.yml .config/default.yml
|
run: cp .config/example.yml .config/default.yml
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
- name : Migrate
|
- name: Generate API JSON
|
||||||
run: pnpm migrate
|
run: pnpm --filter backend generate-api-json
|
||||||
- name: Launch misskey
|
- name: Copy API.json
|
||||||
run: |
|
run: cp packages/backend/built/api.json ${{ matrix.api-json-name }}
|
||||||
screen -S misskey -dm pnpm run dev
|
|
||||||
sleep 30s
|
|
||||||
- name: Wait for Misskey to be ready
|
|
||||||
run: |
|
|
||||||
MAX_RETRIES=12
|
|
||||||
RETRY_DELAY=5
|
|
||||||
count=0
|
|
||||||
until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do
|
|
||||||
printf '.'
|
|
||||||
sleep $RETRY_DELAY
|
|
||||||
count=$((count + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ $count -eq $MAX_RETRIES ]]; then
|
|
||||||
echo "Failed to connect to Misskey after $MAX_RETRIES attempts."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- id: fetch
|
|
||||||
name: Get api.json from Misskey
|
|
||||||
run: |
|
|
||||||
RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json)
|
|
||||||
echo $RESULT > api-base.json
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: api-artifact
|
name: api-artifact
|
||||||
path: api-base.json
|
path: ${{ matrix.api-json-name }}
|
||||||
- name: Kill Misskey Job
|
|
||||||
run: screen -S misskey -X quit
|
|
||||||
|
|
||||||
get-head:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [20.5.1]
|
|
||||||
|
|
||||||
services:
|
|
||||||
db:
|
|
||||||
image: postgres:13
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
env:
|
|
||||||
POSTGRES_DB: misskey
|
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
|
||||||
POSTGRES_USER: example-misskey-user
|
|
||||||
POSTGRESS_PASS: example-misskey-pass
|
|
||||||
redis:
|
|
||||||
image: redis:7
|
|
||||||
ports:
|
|
||||||
- 6379:6379
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4.1.1
|
|
||||||
with:
|
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
|
||||||
ref: ${{ github.head_ref }}
|
|
||||||
submodules: true
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
run_install: false
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v4.0.0
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
cache: 'pnpm'
|
|
||||||
- run: corepack enable
|
|
||||||
- run: pnpm i --frozen-lockfile
|
|
||||||
- name: Check pnpm-lock.yaml
|
|
||||||
run: git diff --exit-code pnpm-lock.yaml
|
|
||||||
- name: Copy Configure
|
|
||||||
run: cp .config/example.yml .config/default.yml
|
|
||||||
- name: Build
|
|
||||||
run: pnpm build
|
|
||||||
- name : Migrate
|
|
||||||
run: pnpm migrate
|
|
||||||
- name: Launch misskey
|
|
||||||
run: |
|
|
||||||
screen -S misskey -dm pnpm run dev
|
|
||||||
sleep 30s
|
|
||||||
- name: Wait for Misskey to be ready
|
|
||||||
run: |
|
|
||||||
MAX_RETRIES=12
|
|
||||||
RETRY_DELAY=5
|
|
||||||
count=0
|
|
||||||
until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do
|
|
||||||
printf '.'
|
|
||||||
sleep $RETRY_DELAY
|
|
||||||
count=$((count + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ $count -eq $MAX_RETRIES ]]; then
|
|
||||||
echo "Failed to connect to Misskey after $MAX_RETRIES attempts."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- id: fetch
|
|
||||||
name: Get api.json from Misskey
|
|
||||||
run: |
|
|
||||||
RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json)
|
|
||||||
echo $RESULT > api-head.json
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: api-artifact
|
|
||||||
path: api-head.json
|
|
||||||
- name: Kill Misskey Job
|
|
||||||
run: screen -S misskey -X quit
|
|
||||||
|
|
||||||
save-pr-number:
|
save-pr-number:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
@ -13,7 +13,7 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.5.1]
|
node-version: [20.10.0]
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
|
|
@ -13,7 +13,7 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.5.1]
|
node-version: [20.10.0]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
- uses: actions/checkout@v4.1.1
|
||||||
|
@ -51,7 +51,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.5.1]
|
node-version: [20.10.0]
|
||||||
browser: [chrome]
|
browser: [chrome]
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.5.1]
|
node-version: [20.10.0]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.5.1]
|
node-version: [20.10.0]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
- uses: actions/checkout@v4.1.1
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
20.5.1
|
20.10.0
|
||||||
|
|
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -17,16 +17,28 @@
|
||||||
### General
|
### General
|
||||||
- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
|
- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
|
||||||
- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
|
- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
|
||||||
|
- Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加
|
||||||
|
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Enhance: 絵文字のオートコンプリート機能強化 #12364
|
- Enhance: 絵文字のオートコンプリート機能強化 #12364
|
||||||
|
- Enhance: ユーザーのRawデータを表示するページが復活
|
||||||
|
- Enhance: リアクション選択時に音を鳴らせるように
|
||||||
|
- Enhance: サウンドにドライブのファイルを使用できるように
|
||||||
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
|
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
|
||||||
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
|
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
|
||||||
|
- Fix: コードエディタが正しく表示されない問題を修正
|
||||||
|
- Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正
|
||||||
|
- Fix: 一度に大量の通知が入った際に通知音が音割れする問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
|
||||||
- Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303
|
- Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303
|
||||||
- Fix: ロールタイムラインが保存されない問題を修正
|
- Fix: ロールタイムラインが保存されない問題を修正
|
||||||
- Fix: api.jsonの生成ロジックを改善 #12402
|
- Fix: api.jsonの生成ロジックを改善 #12402
|
||||||
|
- Fix: 招待コードが使い回せる問題を修正
|
||||||
|
- Fix: 特定の条件下でチャンネルやユーザーのノート一覧に最新のノートが表示されなくなる問題を修正
|
||||||
|
- Fix: 何もノートしていないユーザーのフィードにアクセスするとエラーになる問題を修正
|
||||||
|
|
||||||
## 2023.11.1
|
## 2023.11.1
|
||||||
|
|
||||||
|
@ -43,6 +55,7 @@
|
||||||
- 例: `$[unixtime 1701356400]`
|
- 例: `$[unixtime 1701356400]`
|
||||||
- Enhance: プラグインでエラーが発生した場合のハンドリングを強化
|
- Enhance: プラグインでエラーが発生した場合のハンドリングを強化
|
||||||
- Enhance: 細かなUIのブラッシュアップ
|
- Enhance: 細かなUIのブラッシュアップ
|
||||||
|
- Enhance: サウンド設定に「サウンドを出力しない」と「Misskeyがアクティブな時のみサウンドを出力する」を追加
|
||||||
- Fix: 効果音が再生されるとデバイスで再生している動画や音声が停止する問題を修正 #12339
|
- Fix: 効果音が再生されるとデバイスで再生している動画や音声が停止する問題を修正 #12339
|
||||||
- Fix: デッキに表示されたチャンネルの表示先チャンネルを切り替えた際、即座に反映されない問題を修正 #12236
|
- Fix: デッキに表示されたチャンネルの表示先チャンネルを切り替えた際、即座に反映されない問題を修正 #12236
|
||||||
- Fix: プラグインでノートの表示を書き換えられない問題を修正
|
- Fix: プラグインでノートの表示を書き換えられない問題を修正
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# syntax = docker/dockerfile:1.4
|
# syntax = docker/dockerfile:1.4
|
||||||
|
|
||||||
ARG NODE_VERSION=20.5.1-bullseye
|
ARG NODE_VERSION=20.10.0-bullseye
|
||||||
|
|
||||||
# build assets & compile TypeScript
|
# build assets & compile TypeScript
|
||||||
|
|
||||||
|
|
|
@ -547,6 +547,8 @@ export interface Locale {
|
||||||
"popout": string;
|
"popout": string;
|
||||||
"volume": string;
|
"volume": string;
|
||||||
"masterVolume": string;
|
"masterVolume": string;
|
||||||
|
"notUseSound": string;
|
||||||
|
"useSoundOnlyWhenActive": string;
|
||||||
"details": string;
|
"details": string;
|
||||||
"chooseEmoji": string;
|
"chooseEmoji": string;
|
||||||
"unableToProcess": string;
|
"unableToProcess": string;
|
||||||
|
@ -643,6 +645,7 @@ export interface Locale {
|
||||||
"smtpSecureInfo": string;
|
"smtpSecureInfo": string;
|
||||||
"testEmail": string;
|
"testEmail": string;
|
||||||
"wordMute": string;
|
"wordMute": string;
|
||||||
|
"hardWordMute": string;
|
||||||
"regexpError": string;
|
"regexpError": string;
|
||||||
"regexpErrorDescription": string;
|
"regexpErrorDescription": string;
|
||||||
"instanceMute": string;
|
"instanceMute": string;
|
||||||
|
@ -1040,6 +1043,7 @@ export interface Locale {
|
||||||
"enableChartsForFederatedInstances": string;
|
"enableChartsForFederatedInstances": string;
|
||||||
"showClipButtonInNoteFooter": string;
|
"showClipButtonInNoteFooter": string;
|
||||||
"reactionsDisplaySize": string;
|
"reactionsDisplaySize": string;
|
||||||
|
"limitWidthOfReaction": string;
|
||||||
"noteIdOrUrl": string;
|
"noteIdOrUrl": string;
|
||||||
"video": string;
|
"video": string;
|
||||||
"videos": string;
|
"videos": string;
|
||||||
|
@ -1642,7 +1646,9 @@ export interface Locale {
|
||||||
"assignTarget": string;
|
"assignTarget": string;
|
||||||
"descriptionOfAssignTarget": string;
|
"descriptionOfAssignTarget": string;
|
||||||
"manual": string;
|
"manual": string;
|
||||||
|
"manualRoles": string;
|
||||||
"conditional": string;
|
"conditional": string;
|
||||||
|
"conditionalRoles": string;
|
||||||
"condition": string;
|
"condition": string;
|
||||||
"isConditionalRole": string;
|
"isConditionalRole": string;
|
||||||
"isPublic": string;
|
"isPublic": string;
|
||||||
|
@ -1941,6 +1947,15 @@ export interface Locale {
|
||||||
"notification": string;
|
"notification": string;
|
||||||
"antenna": string;
|
"antenna": string;
|
||||||
"channel": string;
|
"channel": string;
|
||||||
|
"reaction": string;
|
||||||
|
};
|
||||||
|
"_soundSettings": {
|
||||||
|
"driveFile": string;
|
||||||
|
"driveFileWarn": string;
|
||||||
|
"driveFileTypeWarn": string;
|
||||||
|
"driveFileTypeWarnDescription": string;
|
||||||
|
"driveFileDurationWarn": string;
|
||||||
|
"driveFileDurationWarnDescription": string;
|
||||||
};
|
};
|
||||||
"_ago": {
|
"_ago": {
|
||||||
"future": string;
|
"future": string;
|
||||||
|
|
|
@ -544,6 +544,8 @@ showInPage: "ページで表示"
|
||||||
popout: "ポップアウト"
|
popout: "ポップアウト"
|
||||||
volume: "音量"
|
volume: "音量"
|
||||||
masterVolume: "マスター音量"
|
masterVolume: "マスター音量"
|
||||||
|
notUseSound: "サウンドを出力しない"
|
||||||
|
useSoundOnlyWhenActive: "Misskeyがアクティブな時のみサウンドを出力する"
|
||||||
details: "詳細"
|
details: "詳細"
|
||||||
chooseEmoji: "絵文字を選択"
|
chooseEmoji: "絵文字を選択"
|
||||||
unableToProcess: "操作を完了できません"
|
unableToProcess: "操作を完了できません"
|
||||||
|
@ -640,6 +642,7 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
|
||||||
smtpSecureInfo: "STARTTLS使用時はオフにします。"
|
smtpSecureInfo: "STARTTLS使用時はオフにします。"
|
||||||
testEmail: "配信テスト"
|
testEmail: "配信テスト"
|
||||||
wordMute: "ワードミュート"
|
wordMute: "ワードミュート"
|
||||||
|
hardWordMute: "ハードワードミュート"
|
||||||
regexpError: "正規表現エラー"
|
regexpError: "正規表現エラー"
|
||||||
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
|
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
|
||||||
instanceMute: "サーバーミュート"
|
instanceMute: "サーバーミュート"
|
||||||
|
@ -1037,6 +1040,7 @@ enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
|
||||||
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
|
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
|
||||||
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
||||||
reactionsDisplaySize: "リアクションの表示サイズ"
|
reactionsDisplaySize: "リアクションの表示サイズ"
|
||||||
|
limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小して表示する"
|
||||||
noteIdOrUrl: "ノートIDまたはURL"
|
noteIdOrUrl: "ノートIDまたはURL"
|
||||||
video: "動画"
|
video: "動画"
|
||||||
videos: "動画"
|
videos: "動画"
|
||||||
|
@ -1552,7 +1556,9 @@ _role:
|
||||||
assignTarget: "アサイン"
|
assignTarget: "アサイン"
|
||||||
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
|
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
|
||||||
manual: "マニュアル"
|
manual: "マニュアル"
|
||||||
|
manualRoles: "マニュアルロール"
|
||||||
conditional: "コンディショナル"
|
conditional: "コンディショナル"
|
||||||
|
conditionalRoles: "コンディショナルロール"
|
||||||
condition: "条件"
|
condition: "条件"
|
||||||
isConditionalRole: "これはコンディショナルロールです。"
|
isConditionalRole: "これはコンディショナルロールです。"
|
||||||
isPublic: "公開ロール"
|
isPublic: "公開ロール"
|
||||||
|
@ -1846,6 +1852,15 @@ _sfx:
|
||||||
notification: "通知"
|
notification: "通知"
|
||||||
antenna: "アンテナ受信"
|
antenna: "アンテナ受信"
|
||||||
channel: "チャンネル通知"
|
channel: "チャンネル通知"
|
||||||
|
reaction: "リアクション選択時"
|
||||||
|
|
||||||
|
_soundSettings:
|
||||||
|
driveFile: "ドライブの音声を使用"
|
||||||
|
driveFileWarn: "ドライブのファイルを選択してください"
|
||||||
|
driveFileTypeWarn: "このファイルは対応していません"
|
||||||
|
driveFileTypeWarnDescription: "音声ファイルを選択してください"
|
||||||
|
driveFileDurationWarn: "音声が長すぎます"
|
||||||
|
driveFileDurationWarnDescription: "長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか?"
|
||||||
|
|
||||||
_ago:
|
_ago:
|
||||||
future: "未来"
|
future: "未来"
|
||||||
|
@ -1857,7 +1872,7 @@ _ago:
|
||||||
weeksAgo: "{n}週間前"
|
weeksAgo: "{n}週間前"
|
||||||
monthsAgo: "{n}ヶ月前"
|
monthsAgo: "{n}ヶ月前"
|
||||||
yearsAgo: "{n}年前"
|
yearsAgo: "{n}年前"
|
||||||
invalid: "ありません"
|
invalid: "日時の解析に失敗"
|
||||||
|
|
||||||
_timeIn:
|
_timeIn:
|
||||||
seconds: "{n}秒後"
|
seconds: "{n}秒後"
|
||||||
|
|
10
package.json
10
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2023.11.1",
|
"version": "2023.12.0-beta.1",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -52,11 +52,11 @@
|
||||||
"typescript": "5.3.2"
|
"typescript": "5.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "6.11.0",
|
"@typescript-eslint/eslint-plugin": "6.12.0",
|
||||||
"@typescript-eslint/parser": "6.11.0",
|
"@typescript-eslint/parser": "6.12.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.5.1",
|
"cypress": "13.6.0",
|
||||||
"eslint": "8.53.0",
|
"eslint": "8.54.0",
|
||||||
"start-server-and-test": "2.0.3"
|
"start-server-and-test": "2.0.3"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class HardMute1700383825690 {
|
||||||
|
name = 'HardMute1700383825690'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,9 +60,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@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",
|
||||||
"@bull-board/api": "5.9.1",
|
"@bull-board/api": "5.9.2",
|
||||||
"@bull-board/fastify": "5.9.1",
|
"@bull-board/fastify": "5.9.2",
|
||||||
"@bull-board/ui": "5.9.1",
|
"@bull-board/ui": "5.9.2",
|
||||||
"@discordapp/twemoji": "14.1.2",
|
"@discordapp/twemoji": "14.1.2",
|
||||||
"@fastify/accepts": "4.2.0",
|
"@fastify/accepts": "4.2.0",
|
||||||
"@fastify/cookie": "9.2.0",
|
"@fastify/cookie": "9.2.0",
|
||||||
|
@ -72,15 +72,15 @@
|
||||||
"@fastify/multipart": "8.0.0",
|
"@fastify/multipart": "8.0.0",
|
||||||
"@fastify/static": "6.12.0",
|
"@fastify/static": "6.12.0",
|
||||||
"@fastify/view": "8.2.0",
|
"@fastify/view": "8.2.0",
|
||||||
"@nestjs/common": "10.2.8",
|
"@nestjs/common": "10.2.10",
|
||||||
"@nestjs/core": "10.2.8",
|
"@nestjs/core": "10.2.10",
|
||||||
"@nestjs/testing": "10.2.8",
|
"@nestjs/testing": "10.2.10",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@simplewebauthn/server": "8.3.5",
|
"@simplewebauthn/server": "8.3.5",
|
||||||
"@sinonjs/fake-timers": "11.2.2",
|
"@sinonjs/fake-timers": "11.2.2",
|
||||||
"@smithy/node-http-handler": "2.1.5",
|
"@smithy/node-http-handler": "2.1.10",
|
||||||
"@swc/cli": "0.1.63",
|
"@swc/cli": "0.1.63",
|
||||||
"@swc/core": "1.3.96",
|
"@swc/core": "1.3.99",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
"archiver": "6.0.1",
|
"archiver": "6.0.1",
|
||||||
|
@ -88,7 +88,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.13.3",
|
"bullmq": "4.14.2",
|
||||||
"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",
|
||||||
|
@ -114,11 +114,11 @@
|
||||||
"ipaddr.js": "2.1.0",
|
"ipaddr.js": "2.1.0",
|
||||||
"is-svg": "5.0.0",
|
"is-svg": "5.0.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "22.1.0",
|
"jsdom": "23.0.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "8.3.1",
|
"jsonld": "8.3.1",
|
||||||
"jsrsasign": "10.8.6",
|
"jsrsasign": "10.9.0",
|
||||||
"meilisearch": "0.35.0",
|
"meilisearch": "0.36.0",
|
||||||
"mfm-js": "0.23.3",
|
"mfm-js": "0.23.3",
|
||||||
"microformats-parser": "1.5.2",
|
"microformats-parser": "1.5.2",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
|
@ -145,7 +145,7 @@
|
||||||
"qrcode": "1.5.3",
|
"qrcode": "1.5.3",
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"ratelimiter": "3.4.1",
|
"ratelimiter": "3.4.1",
|
||||||
"re2": "1.20.8",
|
"re2": "1.20.9",
|
||||||
"redis-lock": "0.1.4",
|
"redis-lock": "0.1.4",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rename": "1.0.4",
|
"rename": "1.0.4",
|
||||||
|
@ -159,7 +159,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.17",
|
"systeminformation": "5.21.18",
|
||||||
"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",
|
||||||
|
@ -178,7 +178,7 @@
|
||||||
"@simplewebauthn/typescript-types": "8.3.4",
|
"@simplewebauthn/typescript-types": "8.3.4",
|
||||||
"@swc/jest": "0.2.29",
|
"@swc/jest": "0.2.29",
|
||||||
"@types/accepts": "1.3.7",
|
"@types/accepts": "1.3.7",
|
||||||
"@types/archiver": "6.0.1",
|
"@types/archiver": "6.0.2",
|
||||||
"@types/bcryptjs": "2.4.6",
|
"@types/bcryptjs": "2.4.6",
|
||||||
"@types/body-parser": "1.19.5",
|
"@types/body-parser": "1.19.5",
|
||||||
"@types/cbor": "6.0.0",
|
"@types/cbor": "6.0.0",
|
||||||
|
@ -186,28 +186,28 @@
|
||||||
"@types/content-disposition": "0.5.8",
|
"@types/content-disposition": "0.5.8",
|
||||||
"@types/fluent-ffmpeg": "2.1.24",
|
"@types/fluent-ffmpeg": "2.1.24",
|
||||||
"@types/http-link-header": "1.0.5",
|
"@types/http-link-header": "1.0.5",
|
||||||
"@types/jest": "29.5.8",
|
"@types/jest": "29.5.10",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsdom": "21.1.5",
|
"@types/jsdom": "21.1.6",
|
||||||
"@types/jsonld": "1.5.12",
|
"@types/jsonld": "1.5.13",
|
||||||
"@types/jsrsasign": "10.5.12",
|
"@types/jsrsasign": "10.5.12",
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/ms": "0.7.34",
|
"@types/ms": "0.7.34",
|
||||||
"@types/node": "20.9.1",
|
"@types/node": "20.10.0",
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.14",
|
"@types/nodemailer": "6.4.14",
|
||||||
"@types/oauth": "0.9.4",
|
"@types/oauth": "0.9.4",
|
||||||
"@types/oauth2orize": "1.11.3",
|
"@types/oauth2orize": "1.11.3",
|
||||||
"@types/oauth2orize-pkce": "0.1.2",
|
"@types/oauth2orize-pkce": "0.1.2",
|
||||||
"@types/pg": "8.10.9",
|
"@types/pg": "8.10.9",
|
||||||
"@types/pug": "2.0.9",
|
"@types/pug": "2.0.10",
|
||||||
"@types/punycode": "2.1.2",
|
"@types/punycode": "2.1.3",
|
||||||
"@types/qrcode": "1.5.5",
|
"@types/qrcode": "1.5.5",
|
||||||
"@types/random-seed": "0.3.5",
|
"@types/random-seed": "0.3.5",
|
||||||
"@types/ratelimiter": "3.4.6",
|
"@types/ratelimiter": "3.4.6",
|
||||||
"@types/rename": "1.0.7",
|
"@types/rename": "1.0.7",
|
||||||
"@types/sanitize-html": "2.9.4",
|
"@types/sanitize-html": "2.9.5",
|
||||||
"@types/semver": "7.5.5",
|
"@types/semver": "7.5.6",
|
||||||
"@types/sharp": "0.32.0",
|
"@types/sharp": "0.32.0",
|
||||||
"@types/simple-oauth2": "5.0.7",
|
"@types/simple-oauth2": "5.0.7",
|
||||||
"@types/sinonjs__fake-timers": "8.1.5",
|
"@types/sinonjs__fake-timers": "8.1.5",
|
||||||
|
@ -215,12 +215,12 @@
|
||||||
"@types/tmp": "0.2.6",
|
"@types/tmp": "0.2.6",
|
||||||
"@types/vary": "1.1.3",
|
"@types/vary": "1.1.3",
|
||||||
"@types/web-push": "3.6.3",
|
"@types/web-push": "3.6.3",
|
||||||
"@types/ws": "8.5.9",
|
"@types/ws": "8.5.10",
|
||||||
"@typescript-eslint/eslint-plugin": "6.11.0",
|
"@typescript-eslint/eslint-plugin": "6.12.0",
|
||||||
"@typescript-eslint/parser": "6.11.0",
|
"@typescript-eslint/parser": "6.12.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.53.0",
|
"eslint": "8.54.0",
|
||||||
"eslint-plugin-import": "2.29.0",
|
"eslint-plugin-import": "2.29.0",
|
||||||
"execa": "8.0.1",
|
"execa": "8.0.1",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.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 funoutTimelineService: FunoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
) {
|
) {
|
||||||
this.antennasFetched = false;
|
this.antennasFetched = false;
|
||||||
this.antennas = [];
|
this.antennas = [];
|
||||||
|
@ -94,7 +94,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.funoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline);
|
this.fanoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline);
|
||||||
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,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 { FunoutTimelineService } from './FunoutTimelineService.js';
|
import { FanoutTimelineService } from './FanoutTimelineService.js';
|
||||||
import { ChannelFollowingService } from './ChannelFollowingService.js';
|
import { ChannelFollowingService } from './ChannelFollowingService.js';
|
||||||
import { RegistryApiService } from './RegistryApiService.js';
|
import { RegistryApiService } from './RegistryApiService.js';
|
||||||
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
||||||
|
@ -194,7 +194,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 $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService };
|
const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService };
|
||||||
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
|
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
|
||||||
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
||||||
|
|
||||||
|
@ -330,7 +330,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
SearchService,
|
SearchService,
|
||||||
ClipService,
|
ClipService,
|
||||||
FeaturedService,
|
FeaturedService,
|
||||||
FunoutTimelineService,
|
FanoutTimelineService,
|
||||||
ChannelFollowingService,
|
ChannelFollowingService,
|
||||||
RegistryApiService,
|
RegistryApiService,
|
||||||
ChartLoggerService,
|
ChartLoggerService,
|
||||||
|
@ -459,7 +459,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$SearchService,
|
$SearchService,
|
||||||
$ClipService,
|
$ClipService,
|
||||||
$FeaturedService,
|
$FeaturedService,
|
||||||
$FunoutTimelineService,
|
$FanoutTimelineService,
|
||||||
$ChannelFollowingService,
|
$ChannelFollowingService,
|
||||||
$RegistryApiService,
|
$RegistryApiService,
|
||||||
$ChartLoggerService,
|
$ChartLoggerService,
|
||||||
|
@ -589,7 +589,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
SearchService,
|
SearchService,
|
||||||
ClipService,
|
ClipService,
|
||||||
FeaturedService,
|
FeaturedService,
|
||||||
FunoutTimelineService,
|
FanoutTimelineService,
|
||||||
ChannelFollowingService,
|
ChannelFollowingService,
|
||||||
RegistryApiService,
|
RegistryApiService,
|
||||||
FederationChart,
|
FederationChart,
|
||||||
|
@ -717,7 +717,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$SearchService,
|
$SearchService,
|
||||||
$ClipService,
|
$ClipService,
|
||||||
$FeaturedService,
|
$FeaturedService,
|
||||||
$FunoutTimelineService,
|
$FanoutTimelineService,
|
||||||
$ChannelFollowingService,
|
$ChannelFollowingService,
|
||||||
$RegistryApiService,
|
$RegistryApiService,
|
||||||
$FederationChart,
|
$FederationChart,
|
||||||
|
|
|
@ -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 FunoutTimelineService {
|
export class FanoutTimelineService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisForTimelines)
|
@Inject(DI.redisForTimelines)
|
||||||
private redisForTimelines: Redis.Redis,
|
private redisForTimelines: Redis.Redis,
|
|
@ -5,11 +5,12 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import type { MiNote, MiUser } from '@/models/_.js';
|
import type { MiGalleryPost, MiNote, MiUser } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
|
const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
|
||||||
|
export const GALLERY_POSTS_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
|
||||||
const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと
|
const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと
|
||||||
const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ごと
|
const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ごと
|
||||||
|
|
||||||
|
@ -79,6 +80,11 @@ export class FeaturedService {
|
||||||
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
|
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public updateGalleryPostsRanking(galleryPostId: MiGalleryPost['id'], score = 1): Promise<void> {
|
||||||
|
return this.updateRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, galleryPostId, score);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise<void> {
|
public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise<void> {
|
||||||
return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
|
return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
|
||||||
|
@ -99,6 +105,11 @@ export class FeaturedService {
|
||||||
return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, threshold);
|
return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getGalleryPostsRanking(threshold: number): Promise<MiGalleryPost['id'][]> {
|
||||||
|
return this.getRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, threshold);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise<MiNote['id'][]> {
|
public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise<MiNote['id'][]> {
|
||||||
return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold);
|
return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold);
|
||||||
|
|
|
@ -250,6 +250,12 @@ export class MfmService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fnDefault(node: mfm.MfmFn) {
|
||||||
|
const el = doc.createElement('i');
|
||||||
|
appendChildren(node.children, el);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
|
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
|
||||||
bold: (node) => {
|
bold: (node) => {
|
||||||
const el = doc.createElement('b');
|
const el = doc.createElement('b');
|
||||||
|
@ -276,17 +282,68 @@ export class MfmService {
|
||||||
},
|
},
|
||||||
|
|
||||||
fn: (node) => {
|
fn: (node) => {
|
||||||
if (node.props.name === 'unixtime') {
|
switch (node.props.name) {
|
||||||
const text = node.children[0]!.type === 'text' ? node.children[0].props.text : '';
|
case 'unixtime': {
|
||||||
const date = new Date(parseInt(text, 10) * 1000);
|
const text = node.children[0].type === 'text' ? node.children[0].props.text : '';
|
||||||
const el = doc.createElement('time');
|
try {
|
||||||
el.setAttribute('datetime', date.toISOString());
|
const date = new Date(parseInt(text, 10) * 1000);
|
||||||
el.textContent = date.toISOString();
|
const el = doc.createElement('time');
|
||||||
return el;
|
el.setAttribute('datetime', date.toISOString());
|
||||||
} else {
|
el.textContent = date.toISOString();
|
||||||
const el = doc.createElement('i');
|
return el;
|
||||||
appendChildren(node.children, el);
|
} catch (err) {
|
||||||
return el;
|
return fnDefault(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ruby': {
|
||||||
|
if (node.children.length === 1) {
|
||||||
|
const child = node.children[0];
|
||||||
|
const text = child.type === 'text' ? child.props.text : '';
|
||||||
|
const rubyEl = doc.createElement('ruby');
|
||||||
|
const rtEl = doc.createElement('rt');
|
||||||
|
|
||||||
|
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
|
||||||
|
const rpStartEl = doc.createElement('rp');
|
||||||
|
rpStartEl.appendChild(doc.createTextNode('('));
|
||||||
|
const rpEndEl = doc.createElement('rp');
|
||||||
|
rpEndEl.appendChild(doc.createTextNode(')'));
|
||||||
|
|
||||||
|
rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
|
||||||
|
rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
|
||||||
|
rubyEl.appendChild(rpStartEl);
|
||||||
|
rubyEl.appendChild(rtEl);
|
||||||
|
rubyEl.appendChild(rpEndEl);
|
||||||
|
return rubyEl;
|
||||||
|
} else {
|
||||||
|
const rt = node.children.at(-1);
|
||||||
|
|
||||||
|
if (!rt) {
|
||||||
|
return fnDefault(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = rt.type === 'text' ? rt.props.text : '';
|
||||||
|
const rubyEl = doc.createElement('ruby');
|
||||||
|
const rtEl = doc.createElement('rt');
|
||||||
|
|
||||||
|
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
|
||||||
|
const rpStartEl = doc.createElement('rp');
|
||||||
|
rpStartEl.appendChild(doc.createTextNode('('));
|
||||||
|
const rpEndEl = doc.createElement('rp');
|
||||||
|
rpEndEl.appendChild(doc.createTextNode(')'));
|
||||||
|
|
||||||
|
appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
|
||||||
|
rtEl.appendChild(doc.createTextNode(text.trim()));
|
||||||
|
rubyEl.appendChild(rpStartEl);
|
||||||
|
rubyEl.appendChild(rtEl);
|
||||||
|
rubyEl.appendChild(rpEndEl);
|
||||||
|
return rubyEl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return fnDefault(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ 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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
|
|
||||||
|
@ -162,7 +162,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 funoutTimelineService: FunoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
private noteReadService: NoteReadService,
|
private noteReadService: NoteReadService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
private relayService: RelayService,
|
private relayService: RelayService,
|
||||||
|
@ -810,9 +810,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
const r = this.redisForTimelines.pipeline();
|
const r = this.redisForTimelines.pipeline();
|
||||||
|
|
||||||
if (note.channelId) {
|
if (note.channelId) {
|
||||||
this.funoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
|
this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
|
||||||
|
|
||||||
this.funoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
this.fanoutTimelineService.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: {
|
||||||
|
@ -822,9 +822,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const channelFollowing of channelFollowings) {
|
for (const channelFollowing of channelFollowings) {
|
||||||
this.funoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.funoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -862,9 +862,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (!following.withReplies) continue;
|
if (!following.withReplies) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.funoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.funoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -880,36 +880,36 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (!userListMembership.withReplies) continue;
|
if (!userListMembership.withReplies) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.funoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
|
this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.funoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
|
this.fanoutTimelineService.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.funoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.funoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
this.fanoutTimelineService.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.funoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
this.fanoutTimelineService.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.funoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
|
this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.funoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
this.fanoutTimelineService.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.funoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
|
this.fanoutTimelineService.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.funoutTimelineService.push('localTimeline', note.id, 1000, r);
|
this.fanoutTimelineService.push('localTimeline', note.id, 1000, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.funoutTimelineService.push('localTimelineWithFiles', note.id, 500, r);
|
this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
export type RolePolicies = {
|
export type RolePolicies = {
|
||||||
|
@ -110,7 +110,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 funoutTimelineService: FunoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
//this.onMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
|
@ -485,7 +485,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
const redisPipeline = this.redisForTimelines.pipeline();
|
const redisPipeline = this.redisForTimelines.pipeline();
|
||||||
|
|
||||||
for (const role of roles) {
|
for (const role of roles) {
|
||||||
this.funoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline);
|
this.fanoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline);
|
||||||
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
|
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ 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 { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import Logger from '../logger.js';
|
import Logger from '../logger.js';
|
||||||
|
|
||||||
const logger = new Logger('following/create');
|
const logger = new Logger('following/create');
|
||||||
|
@ -84,7 +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 fanoutTimelineService: FanoutTimelineService,
|
||||||
private perUserFollowingChart: PerUserFollowingChart,
|
private perUserFollowingChart: PerUserFollowingChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
) {
|
) {
|
||||||
|
@ -305,7 +305,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.funoutTimelineService.purge(`homeTimeline:${follower.id}`);
|
this.fanoutTimelineService.purge(`homeTimeline:${follower.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish followed event
|
// Publish followed event
|
||||||
|
@ -374,7 +374,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.funoutTimelineService.purge(`homeTimeline:${follower.id}`);
|
this.fanoutTimelineService.purge(`homeTimeline:${follower.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
|
|
|
@ -473,6 +473,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||||
unreadNotificationsCount: notificationsInfo?.unreadCount,
|
unreadNotificationsCount: notificationsInfo?.unreadCount,
|
||||||
mutedWords: profile!.mutedWords,
|
mutedWords: profile!.mutedWords,
|
||||||
|
hardMutedWords: profile!.hardMutedWords,
|
||||||
mutedInstances: profile!.mutedInstances,
|
mutedInstances: profile!.mutedInstances,
|
||||||
mutingNotificationTypes: [], // 後方互換性のため
|
mutingNotificationTypes: [], // 後方互換性のため
|
||||||
notificationRecieveConfig: profile!.notificationRecieveConfig,
|
notificationRecieveConfig: profile!.notificationRecieveConfig,
|
||||||
|
|
|
@ -215,7 +215,12 @@ export class MiUserProfile {
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: [],
|
default: [],
|
||||||
})
|
})
|
||||||
public mutedWords: string[][];
|
public mutedWords: (string[] | string)[];
|
||||||
|
|
||||||
|
@Column('jsonb', {
|
||||||
|
default: [],
|
||||||
|
})
|
||||||
|
public hardMutedWords: (string[] | string)[];
|
||||||
|
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: [],
|
default: [],
|
||||||
|
|
|
@ -530,6 +530,18 @@ export const packedMeDetailedOnlySchema = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hardMutedWords: {
|
||||||
|
type: 'array',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
items: {
|
||||||
|
type: 'array',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
mutedInstances: {
|
mutedInstances: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
|
|
|
@ -126,7 +126,7 @@ export class SignupApiService {
|
||||||
code: invitationCode,
|
code: invitationCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ticket == null) {
|
if (ticket == null || ticket.usedById != null) {
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
@ -71,7 +71,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 funoutTimelineService: FunoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
@ -98,7 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
|
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
|
||||||
}
|
}
|
||||||
|
|
||||||
let noteIds = await this.funoutTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId);
|
let noteIds = await this.fanoutTimelineService.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,9 +12,10 @@ 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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.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 { MetaService } from '@/core/MetaService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -69,15 +70,18 @@ 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 funoutTimelineService: FunoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
|
private metaService: MetaService,
|
||||||
) {
|
) {
|
||||||
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);
|
||||||
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
|
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
|
||||||
const isRangeSpecified = untilId != null && sinceId != null;
|
const isRangeSpecified = untilId != null && sinceId != null;
|
||||||
|
|
||||||
|
const serverSettings = await this.metaService.fetch();
|
||||||
|
|
||||||
const channel = await this.channelsRepository.findOneBy({
|
const channel = await this.channelsRepository.findOneBy({
|
||||||
id: ps.channelId,
|
id: ps.channelId,
|
||||||
});
|
});
|
||||||
|
@ -88,14 +92,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
if (me) this.activeUsersChart.read(me);
|
if (me) this.activeUsersChart.read(me);
|
||||||
|
|
||||||
if (isRangeSpecified || sinceId == null) {
|
if (serverSettings.enableFanoutTimeline && (isRangeSpecified || sinceId == null)) {
|
||||||
const [
|
const [
|
||||||
userIdsWhoMeMuting,
|
userIdsWhoMeMuting,
|
||||||
] = me ? await Promise.all([
|
] = me ? await Promise.all([
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
]) : [new Set<string>()];
|
]) : [new Set<string>()];
|
||||||
|
|
||||||
let noteIds = await this.funoutTimelineService.get(`channelTimeline:${channel.id}`, untilId, sinceId);
|
let noteIds = await this.fanoutTimelineService.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) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { GalleryPostsRepository } from '@/models/_.js';
|
import type { GalleryPostsRepository } from '@/models/_.js';
|
||||||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['gallery'],
|
tags: ['gallery'],
|
||||||
|
@ -27,25 +28,49 @@ export const meta = {
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {},
|
properties: {
|
||||||
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@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
|
||||||
|
private galleryPostsRankingCache: string[] = [];
|
||||||
|
private galleryPostsRankingCacheLastFetchedAt = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.galleryPostsRepository)
|
@Inject(DI.galleryPostsRepository)
|
||||||
private galleryPostsRepository: GalleryPostsRepository,
|
private galleryPostsRepository: GalleryPostsRepository,
|
||||||
|
|
||||||
private galleryPostEntityService: GalleryPostEntityService,
|
private galleryPostEntityService: GalleryPostEntityService,
|
||||||
|
private featuredService: FeaturedService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const query = this.galleryPostsRepository.createQueryBuilder('post')
|
let postIds: string[];
|
||||||
.andWhere('post.createdAt > :date', { date: new Date(Date.now() - (1000 * 60 * 60 * 24 * 3)) })
|
if (this.galleryPostsRankingCacheLastFetchedAt !== 0 && (Date.now() - this.galleryPostsRankingCacheLastFetchedAt < 1000 * 60 * 30)) {
|
||||||
.andWhere('post.likedCount > 0')
|
postIds = this.galleryPostsRankingCache;
|
||||||
.orderBy('post.likedCount', 'DESC');
|
} else {
|
||||||
|
postIds = await this.featuredService.getGalleryPostsRanking(100);
|
||||||
|
this.galleryPostsRankingCache = postIds;
|
||||||
|
this.galleryPostsRankingCacheLastFetchedAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
const posts = await query.limit(10).getMany();
|
postIds.sort((a, b) => a > b ? -1 : 1);
|
||||||
|
if (ps.untilId) {
|
||||||
|
postIds = postIds.filter(id => id < ps.untilId!);
|
||||||
|
}
|
||||||
|
postIds = postIds.slice(0, ps.limit);
|
||||||
|
|
||||||
|
if (postIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.galleryPostsRepository.createQueryBuilder('post')
|
||||||
|
.where('post.id IN (:...postIds)', { postIds: postIds });
|
||||||
|
|
||||||
|
const posts = await query.getMany();
|
||||||
|
|
||||||
return await this.galleryPostEntityService.packMany(posts, me);
|
return await this.galleryPostEntityService.packMany(posts, me);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/_.js';
|
import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/_.js';
|
||||||
|
import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
@ -57,6 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
@Inject(DI.galleryLikesRepository)
|
@Inject(DI.galleryLikesRepository)
|
||||||
private galleryLikesRepository: GalleryLikesRepository,
|
private galleryLikesRepository: GalleryLikesRepository,
|
||||||
|
|
||||||
|
private featuredService: FeaturedService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
@ -88,6 +90,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ランキング更新
|
||||||
|
if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) {
|
||||||
|
await this.featuredService.updateGalleryPostsRanking(post.id, 1);
|
||||||
|
}
|
||||||
|
|
||||||
this.galleryPostsRepository.increment({ id: post.id }, 'likedCount', 1);
|
this.galleryPostsRepository.increment({ id: post.id }, 'likedCount', 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/_.js';
|
import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/_.js';
|
||||||
|
import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
|
@ -49,6 +51,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
@Inject(DI.galleryLikesRepository)
|
@Inject(DI.galleryLikesRepository)
|
||||||
private galleryLikesRepository: GalleryLikesRepository,
|
private galleryLikesRepository: GalleryLikesRepository,
|
||||||
|
|
||||||
|
private featuredService: FeaturedService,
|
||||||
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId });
|
const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId });
|
||||||
|
@ -68,6 +73,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
// Delete like
|
// Delete like
|
||||||
await this.galleryLikesRepository.delete(exist.id);
|
await this.galleryLikesRepository.delete(exist.id);
|
||||||
|
|
||||||
|
// ランキング更新
|
||||||
|
if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) {
|
||||||
|
await this.featuredService.updateGalleryPostsRanking(post.id, -1);
|
||||||
|
}
|
||||||
|
|
||||||
this.galleryPostsRepository.decrement({ id: post.id }, 'likedCount', 1);
|
this.galleryPostsRepository.decrement({ id: post.id }, 'likedCount', 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,6 +123,11 @@ export const meta = {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const muteWords = { type: 'array', items: { oneOf: [
|
||||||
|
{ type: 'array', items: { type: 'string' } },
|
||||||
|
{ type: 'string' }
|
||||||
|
] } } as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -171,7 +176,8 @@ export const paramDef = {
|
||||||
autoSensitive: { type: 'boolean' },
|
autoSensitive: { type: 'boolean' },
|
||||||
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||||
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
mutedWords: { type: 'array' },
|
mutedWords: muteWords,
|
||||||
|
hardMutedWords: muteWords,
|
||||||
mutedInstances: { type: 'array', items: {
|
mutedInstances: { type: 'array', items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
|
@ -234,16 +240,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
||||||
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
||||||
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
|
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
|
||||||
if (ps.mutedWords !== undefined) {
|
|
||||||
|
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
|
||||||
// TODO: ちゃんと数える
|
// TODO: ちゃんと数える
|
||||||
const length = JSON.stringify(ps.mutedWords).length;
|
const length = JSON.stringify(mutedWords).length;
|
||||||
if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) {
|
if (length > limit) {
|
||||||
throw new ApiError(meta.errors.tooManyMutedWords);
|
throw new ApiError(meta.errors.tooManyMutedWords);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// validate regular expression syntax
|
function validateMuteWordRegex(mutedWords: (string[] | string)[]) {
|
||||||
ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => {
|
for (const mutedWord of mutedWords) {
|
||||||
const regexp = x.match(/^\/(.+)\/(.*)$/);
|
if (typeof mutedWord !== "string") continue;
|
||||||
|
|
||||||
|
const regexp = mutedWord.match(/^\/(.+)\/(.*)$/);
|
||||||
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
|
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -251,11 +261,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new ApiError(meta.errors.invalidRegexp);
|
throw new ApiError(meta.errors.invalidRegexp);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.mutedWords !== undefined) {
|
||||||
|
checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
||||||
|
validateMuteWordRegex(ps.mutedWords);
|
||||||
|
|
||||||
profileUpdates.mutedWords = ps.mutedWords;
|
profileUpdates.mutedWords = ps.mutedWords;
|
||||||
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
||||||
}
|
}
|
||||||
|
if (ps.hardMutedWords !== undefined) {
|
||||||
|
checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
||||||
|
validateMuteWordRegex(ps.hardMutedWords);
|
||||||
|
profileUpdates.hardMutedWords = ps.hardMutedWords;
|
||||||
|
}
|
||||||
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
|
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
|
||||||
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
|
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
|
||||||
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
||||||
|
|
|
@ -64,16 +64,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noteIds.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||||
if (ps.untilId) {
|
if (ps.untilId) {
|
||||||
noteIds = noteIds.filter(id => id < ps.untilId!);
|
noteIds = noteIds.filter(id => id < ps.untilId!);
|
||||||
}
|
}
|
||||||
noteIds = noteIds.slice(0, ps.limit);
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
|
if (noteIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const query = this.notesRepository.createQueryBuilder('note')
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
|
|
|
@ -14,7 +14,7 @@ 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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
@ -77,7 +77,7 @@ 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 funoutTimelineService: FunoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private userFollowingService: UserFollowingService,
|
private userFollowingService: UserFollowingService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
|
@ -120,20 +120,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
let shouldFallbackToDb = false;
|
let shouldFallbackToDb = false;
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
const [htlNoteIds, ltlNoteIds] = await this.fanoutTimelineService.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.funoutTimelineService.getMulti([
|
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.fanoutTimelineService.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.funoutTimelineService.getMulti([
|
const [htlNoteIds, ltlNoteIds] = await this.fanoutTimelineService.getMulti([
|
||||||
`homeTimeline:${me.id}`,
|
`homeTimeline:${me.id}`,
|
||||||
'localTimeline',
|
'localTimeline',
|
||||||
], untilId, sinceId);
|
], untilId, sinceId);
|
||||||
|
|
|
@ -14,7 +14,7 @@ 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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
|
@ -69,7 +69,7 @@ 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 funoutTimelineService: FunoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
) {
|
) {
|
||||||
|
@ -107,9 +107,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.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
|
noteIds = await this.fanoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
|
||||||
} else {
|
} else {
|
||||||
const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([
|
const [nonReplyNoteIds, replyNoteIds] = await this.fanoutTimelineService.getMulti([
|
||||||
'localTimeline',
|
'localTimeline',
|
||||||
'localTimelineWithReplies',
|
'localTimelineWithReplies',
|
||||||
], untilId, sinceId);
|
], untilId, sinceId);
|
||||||
|
|
|
@ -14,7 +14,7 @@ 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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
@ -65,7 +65,7 @@ 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 funoutTimelineService: FunoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
private userFollowingService: UserFollowingService,
|
private userFollowingService: UserFollowingService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
|
@ -101,7 +101,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.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
|
let noteIds = await this.fanoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
|
||||||
noteIds = noteIds.slice(0, ps.limit);
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
let redisTimeline: MiNote[] = [];
|
let redisTimeline: MiNote[] = [];
|
||||||
|
|
|
@ -13,7 +13,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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
@ -81,7 +81,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 funoutTimelineService: FunoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
) {
|
) {
|
||||||
|
@ -123,7 +123,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.funoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId);
|
let noteIds = await this.fanoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId);
|
||||||
noteIds = noteIds.slice(0, ps.limit);
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
let redisTimeline: MiNote[] = [];
|
let redisTimeline: MiNote[] = [];
|
||||||
|
|
|
@ -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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.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 funoutTimelineService: FunoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
) {
|
) {
|
||||||
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.funoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId);
|
let noteIds = await this.fanoutTimelineService.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,8 @@ 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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -70,7 +71,8 @@ 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 funoutTimelineService: FunoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
|
private metaService: MetaService,
|
||||||
) {
|
) {
|
||||||
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);
|
||||||
|
@ -78,7 +80,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
const isRangeSpecified = untilId != null && sinceId != null;
|
const isRangeSpecified = untilId != null && sinceId != null;
|
||||||
const isSelf = me && (me.id === ps.userId);
|
const isSelf = me && (me.id === ps.userId);
|
||||||
|
|
||||||
if (isRangeSpecified || sinceId == null) {
|
const serverSettings = await this.metaService.fetch();
|
||||||
|
|
||||||
|
if (serverSettings.enableFanoutTimeline && (isRangeSpecified || sinceId == null)) {
|
||||||
const [
|
const [
|
||||||
userIdsWhoMeMuting,
|
userIdsWhoMeMuting,
|
||||||
] = me ? await Promise.all([
|
] = me ? await Promise.all([
|
||||||
|
@ -86,9 +90,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.funoutTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId),
|
this.fanoutTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId),
|
||||||
ps.withReplies ? this.funoutTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
|
ps.withReplies ? this.fanoutTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
|
||||||
ps.withChannelNotes ? this.funoutTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
|
ps.withChannelNotes ? this.fanoutTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let noteIds = Array.from(new Set([
|
let noteIds = Array.from(new Set([
|
||||||
|
|
|
@ -58,7 +58,7 @@ export class FeedService {
|
||||||
const feed = new Feed({
|
const feed = new Feed({
|
||||||
id: author.link,
|
id: author.link,
|
||||||
title: `${author.name} (@${user.username}@${this.config.host})`,
|
title: `${author.name} (@${user.username}@${this.config.host})`,
|
||||||
updated: this.idService.parse(notes[0].id).date,
|
updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined,
|
||||||
generator: 'Misskey',
|
generator: 'Misskey',
|
||||||
description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
|
description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
|
||||||
link: author.link,
|
link: author.link,
|
||||||
|
|
|
@ -93,7 +93,7 @@ describe('Webリソース', () => {
|
||||||
});
|
});
|
||||||
aliceChannel = await channel(alice, {});
|
aliceChannel = await channel(alice, {});
|
||||||
|
|
||||||
bob = await signup({ username: 'alice' });
|
bob = await signup({ username: 'bob' });
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -152,6 +152,11 @@ describe('Webリソース', () => {
|
||||||
type,
|
type,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
test('がGETできる。(ノートが存在しない場合でも。)', async () => await ok({
|
||||||
|
path: path(bob.username),
|
||||||
|
type,
|
||||||
|
}));
|
||||||
|
|
||||||
test('は存在しないユーザーはGETできない。', async () => await notOk({
|
test('は存在しないユーザーはGETできない。', async () => await notOk({
|
||||||
path: path('nonexisting'),
|
path: path('nonexisting'),
|
||||||
status: 404,
|
status: 404,
|
||||||
|
|
|
@ -168,6 +168,7 @@ describe('ユーザー', () => {
|
||||||
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
|
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
|
||||||
unreadAnnouncements: user.unreadAnnouncements,
|
unreadAnnouncements: user.unreadAnnouncements,
|
||||||
mutedWords: user.mutedWords,
|
mutedWords: user.mutedWords,
|
||||||
|
hardMutedWords: user.hardMutedWords,
|
||||||
mutedInstances: user.mutedInstances,
|
mutedInstances: user.mutedInstances,
|
||||||
mutingNotificationTypes: user.mutingNotificationTypes,
|
mutingNotificationTypes: user.mutingNotificationTypes,
|
||||||
notificationRecieveConfig: user.notificationRecieveConfig,
|
notificationRecieveConfig: user.notificationRecieveConfig,
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -9,7 +9,7 @@
|
||||||
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
|
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
|
||||||
"build-storybook": "pnpm build-storybook-pre && storybook build",
|
"build-storybook": "pnpm build-storybook-pre && storybook build",
|
||||||
"chromatic": "chromatic",
|
"chromatic": "chromatic",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run --globals",
|
||||||
"test-and-coverage": "vitest --run --coverage --globals",
|
"test-and-coverage": "vitest --run --coverage --globals",
|
||||||
"typecheck": "vue-tsc --noEmit",
|
"typecheck": "vue-tsc --noEmit",
|
||||||
"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",
|
"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordapp/twemoji": "14.1.2",
|
"@discordapp/twemoji": "14.1.2",
|
||||||
"@github/webauthn-json": "2.1.1",
|
"@github/webauthn-json": "2.1.1",
|
||||||
"@rollup/plugin-alias": "5.0.1",
|
"@rollup/plugin-alias": "5.1.0",
|
||||||
"@rollup/plugin-json": "6.0.1",
|
"@rollup/plugin-json": "6.0.1",
|
||||||
"@rollup/plugin-replace": "5.0.5",
|
"@rollup/plugin-replace": "5.0.5",
|
||||||
"@rollup/pluginutils": "5.0.5",
|
"@rollup/pluginutils": "5.0.5",
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
"@tabler/icons-webfont": "2.37.0",
|
"@tabler/icons-webfont": "2.37.0",
|
||||||
"@vitejs/plugin-vue": "4.5.0",
|
"@vitejs/plugin-vue": "4.5.0",
|
||||||
"@vue-macros/reactivity-transform": "0.4.0",
|
"@vue-macros/reactivity-transform": "0.4.0",
|
||||||
"@vue/compiler-sfc": "3.3.8",
|
"@vue/compiler-sfc": "3.3.9",
|
||||||
"astring": "1.8.6",
|
"astring": "1.8.6",
|
||||||
"autosize": "6.0.1",
|
"autosize": "6.0.1",
|
||||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6",
|
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6",
|
||||||
|
@ -39,7 +39,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": "9.0.0",
|
"chromatic": "9.1.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,7 +57,7 @@
|
||||||
"photoswipe": "5.4.2",
|
"photoswipe": "5.4.2",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"querystring": "0.2.1",
|
"querystring": "0.2.1",
|
||||||
"rollup": "4.4.1",
|
"rollup": "4.6.0",
|
||||||
"sanitize-html": "2.11.0",
|
"sanitize-html": "2.11.0",
|
||||||
"shiki": "^0.14.5",
|
"shiki": "^0.14.5",
|
||||||
"sass": "1.69.5",
|
"sass": "1.69.5",
|
||||||
|
@ -73,8 +73,8 @@
|
||||||
"uuid": "9.0.1",
|
"uuid": "9.0.1",
|
||||||
"v-code-diff": "1.7.2",
|
"v-code-diff": "1.7.2",
|
||||||
"vanilla-tilt": "1.8.1",
|
"vanilla-tilt": "1.8.1",
|
||||||
"vite": "4.5.0",
|
"vite": "5.0.2",
|
||||||
"vue": "3.3.8",
|
"vue": "3.3.9",
|
||||||
"vuedraggable": "next"
|
"vuedraggable": "next"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -96,27 +96,27 @@
|
||||||
"@storybook/types": "7.5.3",
|
"@storybook/types": "7.5.3",
|
||||||
"@storybook/vue3": "7.5.3",
|
"@storybook/vue3": "7.5.3",
|
||||||
"@storybook/vue3-vite": "7.5.3",
|
"@storybook/vue3-vite": "7.5.3",
|
||||||
"@testing-library/vue": "8.0.0",
|
"@testing-library/vue": "8.0.1",
|
||||||
"@types/escape-regexp": "0.0.3",
|
"@types/escape-regexp": "0.0.3",
|
||||||
"@types/estree": "1.0.5",
|
"@types/estree": "1.0.5",
|
||||||
"@types/matter-js": "0.19.4",
|
"@types/matter-js": "0.19.5",
|
||||||
"@types/micromatch": "4.0.5",
|
"@types/micromatch": "4.0.6",
|
||||||
"@types/node": "20.9.1",
|
"@types/node": "20.10.0",
|
||||||
"@types/punycode": "2.1.2",
|
"@types/punycode": "2.1.3",
|
||||||
"@types/sanitize-html": "2.9.4",
|
"@types/sanitize-html": "2.9.5",
|
||||||
"@types/throttle-debounce": "5.0.2",
|
"@types/throttle-debounce": "5.0.2",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@types/uuid": "9.0.7",
|
"@types/uuid": "9.0.7",
|
||||||
"@types/websocket": "1.0.9",
|
"@types/websocket": "1.0.10",
|
||||||
"@types/ws": "8.5.9",
|
"@types/ws": "8.5.10",
|
||||||
"@typescript-eslint/eslint-plugin": "6.11.0",
|
"@typescript-eslint/eslint-plugin": "6.12.0",
|
||||||
"@typescript-eslint/parser": "6.11.0",
|
"@typescript-eslint/parser": "6.12.0",
|
||||||
"@vitest/coverage-v8": "0.34.6",
|
"@vitest/coverage-v8": "0.34.6",
|
||||||
"@vue/runtime-core": "3.3.8",
|
"@vue/runtime-core": "3.3.9",
|
||||||
"acorn": "8.11.2",
|
"acorn": "8.11.2",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.5.1",
|
"cypress": "13.6.0",
|
||||||
"eslint": "8.53.0",
|
"eslint": "8.54.0",
|
||||||
"eslint-plugin-import": "2.29.0",
|
"eslint-plugin-import": "2.29.0",
|
||||||
"eslint-plugin-vue": "9.18.1",
|
"eslint-plugin-vue": "9.18.1",
|
||||||
"fast-glob": "3.3.2",
|
"fast-glob": "3.3.2",
|
||||||
|
|
|
@ -204,12 +204,16 @@ export async function common(createVue: () => App<Element>) {
|
||||||
|
|
||||||
if (defaultStore.state.keepScreenOn) {
|
if (defaultStore.state.keepScreenOn) {
|
||||||
if ('wakeLock' in navigator) {
|
if ('wakeLock' in navigator) {
|
||||||
navigator.wakeLock.request('screen');
|
navigator.wakeLock.request('screen')
|
||||||
|
.then(() => {
|
||||||
document.addEventListener('visibilitychange', async () => {
|
document.addEventListener('visibilitychange', async () => {
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible') {
|
||||||
navigator.wakeLock.request('screen');
|
navigator.wakeLock.request('screen');
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// If Permission fails on an AppleDevice such as Safari
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<Mfm :text="report.comment"/>
|
<Mfm :text="report.comment"/>
|
||||||
</div>
|
</div>
|
||||||
<hr/>
|
<hr/>
|
||||||
<div>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></div>
|
<div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link">@{{ report.reporter.username }}</MkA></div>
|
||||||
<div v-if="report.assignee">
|
<div v-if="report.assignee">
|
||||||
{{ i18n.ts.moderator }}:
|
{{ i18n.ts.moderator }}:
|
||||||
<MkAcct :user="report.assignee"/>
|
<MkAcct :user="report.assignee"/>
|
||||||
|
|
|
@ -265,7 +265,7 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30):
|
||||||
// 前方一致(エイリアスなし)
|
// 前方一致(エイリアスなし)
|
||||||
emojiDb.some(x => {
|
emojiDb.some(x => {
|
||||||
if (x.name.startsWith(query) && !x.aliasOf) {
|
if (x.name.startsWith(query) && !x.aliasOf) {
|
||||||
matched.set(x.name, { emoji: x, score: query.length });
|
matched.set(x.name, { emoji: x, score: query.length + 1 });
|
||||||
}
|
}
|
||||||
return matched.size === max;
|
return matched.size === max;
|
||||||
});
|
});
|
||||||
|
@ -273,8 +273,8 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30):
|
||||||
// 前方一致(エイリアス込み)
|
// 前方一致(エイリアス込み)
|
||||||
if (matched.size < max) {
|
if (matched.size < max) {
|
||||||
emojiDb.some(x => {
|
emojiDb.some(x => {
|
||||||
if (x.name.startsWith(query)) {
|
if (x.name.startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
|
||||||
matched.set(x.name, { emoji: x, score: query.length });
|
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
|
||||||
}
|
}
|
||||||
return matched.size === max;
|
return matched.size === max;
|
||||||
});
|
});
|
||||||
|
@ -283,36 +283,32 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30):
|
||||||
// 部分一致(エイリアス込み)
|
// 部分一致(エイリアス込み)
|
||||||
if (matched.size < max) {
|
if (matched.size < max) {
|
||||||
emojiDb.some(x => {
|
emojiDb.some(x => {
|
||||||
if (x.name.includes(query)) {
|
if (x.name.includes(query) && !matched.has(x.aliasOf ?? x.name)) {
|
||||||
matched.set(x.name, { emoji: x, score: query.length });
|
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
|
||||||
}
|
}
|
||||||
return matched.size === max;
|
return matched.size === max;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 簡易あいまい検索
|
// 簡易あいまい検索(3文字以上)
|
||||||
if (matched.size < max) {
|
if (matched.size < max && query.length > 3) {
|
||||||
const queryChars = [...query];
|
const queryChars = [...query];
|
||||||
const hitEmojis = new Map<string, EmojiScore>();
|
const hitEmojis = new Map<string, EmojiScore>();
|
||||||
|
|
||||||
for (const x of emojiDb) {
|
for (const x of emojiDb) {
|
||||||
// クエリ文字列の1文字単位で絵文字名にヒットするかを見る
|
// 文字列の位置を進めながら、クエリの文字を順番に探す
|
||||||
// ただし、過剰に検出されるのを防ぐためクエリ文字列に登場する順番で絵文字名を走査する
|
|
||||||
|
|
||||||
let queryCharHitPos = 0;
|
let pos = 0;
|
||||||
let queryCharHitCount = 0;
|
let hit = 0;
|
||||||
for (let idx = 0; idx < queryChars.length; idx++) {
|
for (const c of queryChars) {
|
||||||
queryCharHitPos = x.name.indexOf(queryChars[idx], queryCharHitPos);
|
pos = x.name.indexOf(c, pos);
|
||||||
if (queryCharHitPos <= -1) {
|
if (pos <= -1) break;
|
||||||
break;
|
hit++;
|
||||||
}
|
|
||||||
|
|
||||||
queryCharHitCount++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ヒット数が少なすぎると検索結果が汚れるので調節する
|
// 半分以上の文字が含まれていればヒットとする
|
||||||
if (queryCharHitCount > 2) {
|
if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
|
||||||
hitEmojis.set(x.name, { emoji: x, score: queryCharHitCount });
|
hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -139,6 +139,10 @@ watch(v, () => {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.textarea, .codeEditorHighlighter {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -154,6 +158,8 @@ watch(v, () => {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
min-width: calc(100% - 24px);
|
||||||
|
height: calc(100% - 24px);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
|
|
@ -37,6 +37,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import bytes from '@/filters/bytes.js';
|
import bytes from '@/filters/bytes.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import hasAudio from '@/scripts/media-has-audio.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
video: Misskey.entities.DriveFile;
|
video: Misskey.entities.DriveFile;
|
||||||
|
@ -49,6 +50,12 @@ const videoEl = shallowRef<HTMLVideoElement>();
|
||||||
watch(videoEl, () => {
|
watch(videoEl, () => {
|
||||||
if (videoEl.value) {
|
if (videoEl.value) {
|
||||||
videoEl.value.volume = 0.3;
|
videoEl.value.volume = 0.3;
|
||||||
|
hasAudio(videoEl.value).then(had => {
|
||||||
|
if (!had) {
|
||||||
|
videoEl.value.loop = videoEl.value.muted = true;
|
||||||
|
videoEl.value.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="!muted"
|
v-if="!hardMuted && !muted"
|
||||||
v-show="!isDeleted"
|
v-show="!isDeleted"
|
||||||
ref="el"
|
ref="el"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
|
@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div v-else :class="$style.muted" @click="muted = false">
|
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||||
<template #name>
|
<template #name>
|
||||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||||
|
@ -142,6 +142,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
</I18n>
|
</I18n>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<!--
|
||||||
|
MkDateSeparatedList uses TransitionGroup which requires single element in the child elements
|
||||||
|
so MkNote create empty div instead of no elements
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -163,6 +169,7 @@ import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
import * as sound from '@/scripts/sound.js';
|
||||||
import { defaultStore, noteViewInterruptors } from '@/store.js';
|
import { defaultStore, noteViewInterruptors } from '@/store.js';
|
||||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||||
|
@ -183,6 +190,7 @@ const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
mock?: boolean;
|
mock?: boolean;
|
||||||
|
withHardMute?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
mock: false,
|
mock: false,
|
||||||
});
|
});
|
||||||
|
@ -239,13 +247,23 @@ const urls = $computed(() => parsed ? extractUrlFromMfm(parsed) : null);
|
||||||
const isLong = shouldCollapsed(appearNote, urls ?? []);
|
const isLong = shouldCollapsed(appearNote, urls ?? []);
|
||||||
const collapsed = ref(appearNote.cw == null && isLong);
|
const collapsed = ref(appearNote.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
const muted = ref(checkMute(appearNote, $i?.mutedWords));
|
||||||
|
const hardMuted = ref(props.withHardMute && checkMute(appearNote, $i?.hardMutedWords));
|
||||||
const translation = ref<any>(null);
|
const translation = ref<any>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
|
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
|
||||||
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
|
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
|
||||||
|
|
||||||
|
function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
|
||||||
|
if (mutedWords == null) return false;
|
||||||
|
|
||||||
|
if (checkWordMute(note, $i, mutedWords)) return true;
|
||||||
|
if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
|
||||||
|
if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
'r': () => reply(true),
|
||||||
'e|a|plus': () => react(true),
|
'e|a|plus': () => react(true),
|
||||||
|
@ -325,6 +343,8 @@ function react(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
if (appearNote.reactionAcceptance === 'likeOnly') {
|
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||||
|
sound.play('reaction');
|
||||||
|
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -343,6 +363,8 @@ function react(viaKeyboard = false): void {
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value, reaction => {
|
reactionPicker.show(reactButton.value, reaction => {
|
||||||
|
sound.play('reaction');
|
||||||
|
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
emit('reaction', reaction);
|
emit('reaction', reaction);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -210,6 +210,7 @@ import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import { notePage } from '@/filters/note.js';
|
import { notePage } from '@/filters/note.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
import * as sound from '@/scripts/sound.js';
|
||||||
import { defaultStore, noteViewInterruptors } from '@/store.js';
|
import { defaultStore, noteViewInterruptors } from '@/store.js';
|
||||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||||
|
@ -369,6 +370,8 @@ function react(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
if (appearNote.reactionAcceptance === 'likeOnly') {
|
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||||
|
sound.play('reaction');
|
||||||
|
|
||||||
os.api('notes/reactions/create', {
|
os.api('notes/reactions/create', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
|
@ -383,6 +386,8 @@ function react(viaKeyboard = false): void {
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value, reaction => {
|
reactionPicker.show(reactButton.value, reaction => {
|
||||||
|
sound.play('reaction');
|
||||||
|
|
||||||
os.api('notes/reactions/create', {
|
os.api('notes/reactions/create', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
|
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:ad="true"
|
:ad="true"
|
||||||
:class="$style.notes"
|
:class="$style.notes"
|
||||||
>
|
>
|
||||||
<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
|
<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
|
||||||
</MkDateSeparatedList>
|
</MkDateSeparatedList>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template #default="{ items: notifications }">
|
<template #default="{ items: notifications }">
|
||||||
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
|
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
|
||||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/>
|
||||||
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
|
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
|
||||||
</MkDateSeparatedList>
|
</MkDateSeparatedList>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
|
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
|
||||||
@click="toggleReaction()"
|
@click="toggleReaction()"
|
||||||
>
|
>
|
||||||
<MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
<MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
||||||
<span :class="$style.count">{{ count }}</span>
|
<span :class="$style.count">{{ count }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -28,6 +28,7 @@ import MkReactionEffect from '@/components/MkReactionEffect.vue';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import * as sound from '@/scripts/sound.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reaction: string;
|
reaction: string;
|
||||||
|
@ -59,6 +60,10 @@ async function toggleReaction() {
|
||||||
});
|
});
|
||||||
if (confirm.canceled) return;
|
if (confirm.canceled) return;
|
||||||
|
|
||||||
|
if (oldReaction !== props.reaction) {
|
||||||
|
sound.play('reaction');
|
||||||
|
}
|
||||||
|
|
||||||
if (mock) {
|
if (mock) {
|
||||||
emit('reactionToggled', props.reaction, (props.count - 1));
|
emit('reactionToggled', props.reaction, (props.count - 1));
|
||||||
return;
|
return;
|
||||||
|
@ -75,6 +80,8 @@ async function toggleReaction() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
sound.play('reaction');
|
||||||
|
|
||||||
if (mock) {
|
if (mock) {
|
||||||
emit('reactionToggled', props.reaction, (props.count + 1));
|
emit('reactionToggled', props.reaction, (props.count + 1));
|
||||||
return;
|
return;
|
||||||
|
@ -188,7 +195,7 @@ if (!mock) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.limitWidth {
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,12 +28,25 @@ const props = withDefaults(defineProps<{
|
||||||
mode: 'relative',
|
mode: 'relative',
|
||||||
});
|
});
|
||||||
|
|
||||||
const _time = props.time == null ? NaN :
|
function getDateSafe(n: Date | string | number) {
|
||||||
typeof props.time === 'number' ? props.time :
|
try {
|
||||||
(props.time instanceof Date ? props.time : new Date(props.time)).getTime();
|
if (n instanceof Date) {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
return new Date(n);
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
getTime: () => NaN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||||
|
const _time = props.time == null ? NaN : getDateSafe(props.time).getTime();
|
||||||
const invalid = Number.isNaN(_time);
|
const invalid = Number.isNaN(_time);
|
||||||
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
||||||
|
|
||||||
|
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||||
let now = $ref((props.origin ?? new Date()).getTime());
|
let now = $ref((props.origin ?? new Date()).getTime());
|
||||||
const ago = $computed(() => (now - _time) / 1000/*ms*/);
|
const ago = $computed(() => (now - _time) / 1000/*ms*/);
|
||||||
|
|
||||||
|
|
|
@ -206,13 +206,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
|
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkFoldableSection>
|
<MkFoldableSection>
|
||||||
<template #header>Manual roles</template>
|
<template #header>{{ i18n.ts._role.manualRoles }}</template>
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkRolePreview v-for="role in roles.filter(x => x.target === 'manual')" :key="role.id" :role="role" :forModeration="true"/>
|
<MkRolePreview v-for="role in roles.filter(x => x.target === 'manual')" :key="role.id" :role="role" :forModeration="true"/>
|
||||||
</div>
|
</div>
|
||||||
</MkFoldableSection>
|
</MkFoldableSection>
|
||||||
<MkFoldableSection>
|
<MkFoldableSection>
|
||||||
<template #header>Conditional roles</template>
|
<template #header>{{ i18n.ts._role.conditionalRoles }}</template>
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkRolePreview v-for="role in roles.filter(x => x.target === 'conditional')" :key="role.id" :role="role" :forModeration="true"/>
|
<MkRolePreview v-for="role in roles.filter(x => x.target === 'conditional')" :key="role.id" :role="role" :forModeration="true"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -56,6 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<option value="medium">{{ i18n.ts.medium }}</option>
|
<option value="medium">{{ i18n.ts.medium }}</option>
|
||||||
<option value="large">{{ i18n.ts.large }}</option>
|
<option value="large">{{ i18n.ts.large }}</option>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
|
<MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkSelect v-model="instanceTicker">
|
<MkSelect v-model="instanceTicker">
|
||||||
|
@ -226,6 +227,7 @@ const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serve
|
||||||
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
|
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
|
||||||
const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
|
const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
|
||||||
const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
|
const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
|
||||||
|
const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction'));
|
||||||
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
|
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
|
||||||
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
|
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
|
||||||
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
|
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
|
||||||
|
@ -290,6 +292,7 @@ watch([
|
||||||
overridedDeviceKind,
|
overridedDeviceKind,
|
||||||
mediaListWithOneImageAppearance,
|
mediaListWithOneImageAppearance,
|
||||||
reactionsDisplaySize,
|
reactionsDisplaySize,
|
||||||
|
limitWidthOfReaction,
|
||||||
highlightSensitiveMedia,
|
highlightSensitiveMedia,
|
||||||
keepScreenOn,
|
keepScreenOn,
|
||||||
disableStreamingTimeline,
|
disableStreamingTimeline,
|
||||||
|
|
|
@ -9,7 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #icon><i class="ti ti-message-off"></i></template>
|
<template #icon><i class="ti ti-message-off"></i></template>
|
||||||
<template #label>{{ i18n.ts.wordMute }}</template>
|
<template #label>{{ i18n.ts.wordMute }}</template>
|
||||||
|
|
||||||
<XWordMute/>
|
<XWordMute :muted="$i!.mutedWords" @save="saveMutedWords"/>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder>
|
||||||
|
<template #icon><i class="ti ti-message-off"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.hardWordMute }}</template>
|
||||||
|
|
||||||
|
<XWordMute :muted="$i!.hardMutedWords" @save="saveHardMutedWords"/>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder>
|
<MkFolder>
|
||||||
|
@ -129,6 +136,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
|
||||||
const renoteMutingPagination = {
|
const renoteMutingPagination = {
|
||||||
|
@ -207,6 +215,14 @@ async function toggleBlockItem(item) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveMutedWords(mutedWords: (string | string[])[]) {
|
||||||
|
await os.api('i/update', { mutedWords });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveHardMutedWords(hardMutedWords: (string | string[])[]) {
|
||||||
|
await os.api('i/update', { hardMutedWords });
|
||||||
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
const headerActions = $computed(() => []);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => []);
|
||||||
|
|
|
@ -18,16 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
|
||||||
import MkTab from '@/components/MkTab.vue';
|
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import number from '@/filters/number.js';
|
|
||||||
import { defaultStore } from '@/store.js';
|
|
||||||
import { $i } from '@/account.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|
||||||
|
const props = defineProps<{
|
||||||
|
muted: (string[] | string)[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'save', value: (string[] | string)[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
const render = (mutedWords) => mutedWords.map(x => {
|
const render = (mutedWords) => mutedWords.map(x => {
|
||||||
if (Array.isArray(x)) {
|
if (Array.isArray(x)) {
|
||||||
|
@ -37,8 +38,7 @@ const render = (mutedWords) => mutedWords.map(x => {
|
||||||
}
|
}
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
|
||||||
const tab = ref('soft');
|
const mutedWords = ref(render(props.muted));
|
||||||
const mutedWords = ref(render($i!.mutedWords));
|
|
||||||
const changed = ref(false);
|
const changed = ref(false);
|
||||||
|
|
||||||
watch(mutedWords, () => {
|
watch(mutedWords, () => {
|
||||||
|
@ -85,9 +85,7 @@ async function save() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await os.api('i/update', {
|
emit('save', parsed);
|
||||||
mutedWords: parsed,
|
|
||||||
});
|
|
||||||
|
|
||||||
changed.value = false;
|
changed.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkSelect v-model="type">
|
<MkSelect v-model="type">
|
||||||
<template #label>{{ i18n.ts.sound }}</template>
|
<template #label>{{ i18n.ts.sound }}</template>
|
||||||
<option v-for="x in soundsTypes" :key="x" :value="x">{{ x == null ? i18n.ts.none : x }}</option>
|
<option v-for="x in soundsTypes" :key="x ?? 'null'" :value="x">{{ getSoundTypeName(x) }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
<div v-if="type === '_driveFile_'" :class="$style.fileSelectorRoot">
|
||||||
|
<MkButton :class="$style.fileSelectorButton" inline rounded primary @click="selectSound">{{ i18n.ts.selectFile }}</MkButton>
|
||||||
|
<div :class="['_nowrap', !fileUrl && $style.fileNotSelected]">{{ friendlyFileName }}</div>
|
||||||
|
</div>
|
||||||
<MkRange v-model="volume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
|
<MkRange v-model="volume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
|
||||||
<template #label>{{ i18n.ts.volume }}</template>
|
<template #label>{{ i18n.ts.volume }}</template>
|
||||||
</MkRange>
|
</MkRange>
|
||||||
|
@ -21,30 +25,149 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
import type { SoundType } from '@/scripts/sound.js';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkRange from '@/components/MkRange.vue';
|
import MkRange from '@/components/MkRange.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { playFile, soundsTypes } from '@/scripts/sound.js';
|
import * as os from '@/os.js';
|
||||||
|
import { playFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js';
|
||||||
|
import { selectFile } from '@/scripts/select-file.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
type: string;
|
type: SoundType;
|
||||||
|
fileId?: string;
|
||||||
|
fileUrl?: string;
|
||||||
volume: number;
|
volume: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update', result: { type: string; volume: number; }): void;
|
(ev: 'update', result: { type: SoundType; fileId?: string; fileUrl?: string; volume: number; }): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let type = $ref(props.type);
|
const type = ref<SoundType>(props.type);
|
||||||
let volume = $ref(props.volume);
|
const fileId = ref(props.fileId);
|
||||||
|
const fileUrl = ref(props.fileUrl);
|
||||||
|
const fileName = ref<string>('');
|
||||||
|
const volume = ref(props.volume);
|
||||||
|
|
||||||
|
if (type.value === '_driveFile_' && fileId.value) {
|
||||||
|
const apiRes = await os.api('drive/files/show', {
|
||||||
|
fileId: fileId.value,
|
||||||
|
});
|
||||||
|
fileName.value = apiRes.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSoundTypeName(f: SoundType): string {
|
||||||
|
switch (f) {
|
||||||
|
case null:
|
||||||
|
return i18n.ts.none;
|
||||||
|
case '_driveFile_':
|
||||||
|
return i18n.ts._soundSettings.driveFile;
|
||||||
|
default:
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const friendlyFileName = computed<string>(() => {
|
||||||
|
if (fileName.value) {
|
||||||
|
return fileName.value;
|
||||||
|
}
|
||||||
|
if (fileUrl.value) {
|
||||||
|
return fileUrl.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n.ts._soundSettings.driveFileWarn;
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectSound(ev) {
|
||||||
|
selectFile(ev.currentTarget ?? ev.target, i18n.ts._soundSettings.driveFile).then(async (file) => {
|
||||||
|
if (!file.type.startsWith('audio')) {
|
||||||
|
os.alert({
|
||||||
|
type: 'warning',
|
||||||
|
title: i18n.ts._soundSettings.driveFileTypeWarn,
|
||||||
|
text: i18n.ts._soundSettings.driveFileTypeWarnDescription,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const duration = await getSoundDuration(file.url);
|
||||||
|
if (duration >= 2000) {
|
||||||
|
const { canceled } = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
title: i18n.ts._soundSettings.driveFileDurationWarn,
|
||||||
|
text: i18n.ts._soundSettings.driveFileDurationWarnDescription,
|
||||||
|
okText: i18n.ts.continue,
|
||||||
|
cancelText: i18n.ts.cancel,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileUrl.value = file.url;
|
||||||
|
fileName.value = file.name;
|
||||||
|
fileId.value = file.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function listen() {
|
function listen() {
|
||||||
playFile(type, volume);
|
if (type.value === '_driveFile_' && (!fileUrl.value || !fileId.value)) {
|
||||||
|
os.alert({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts._soundSettings.driveFileWarn,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
playFile(type.value === '_driveFile_' ? {
|
||||||
|
type: '_driveFile_',
|
||||||
|
fileId: fileId.value as string,
|
||||||
|
fileUrl: fileUrl.value as string,
|
||||||
|
volume: volume.value,
|
||||||
|
} : {
|
||||||
|
type: type.value,
|
||||||
|
volume: volume.value,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
emit('update', { type, volume });
|
if (type.value === '_driveFile_' && !fileUrl.value) {
|
||||||
|
os.alert({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts._soundSettings.driveFileWarn,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.value !== '_driveFile_') {
|
||||||
|
fileUrl.value = undefined;
|
||||||
|
fileName.value = '';
|
||||||
|
fileId.value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('update', {
|
||||||
|
type: type.value,
|
||||||
|
fileId: fileId.value,
|
||||||
|
fileUrl: fileUrl.value,
|
||||||
|
volume: volume.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
os.success();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.fileSelectorRoot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileSelectorButton {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileNotSelected {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--infoWarnFg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -5,6 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
|
<MkSwitch v-model="notUseSound">
|
||||||
|
<template #label>{{ i18n.ts.notUseSound }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="useSoundOnlyWhenActive">
|
||||||
|
<template #label>{{ i18n.ts.useSoundOnlyWhenActive }}</template>
|
||||||
|
</MkSwitch>
|
||||||
<MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
|
<MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
|
||||||
<template #label>{{ i18n.ts.masterVolume }}</template>
|
<template #label>{{ i18n.ts.masterVolume }}</template>
|
||||||
</MkRange>
|
</MkRange>
|
||||||
|
@ -12,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>{{ i18n.ts.sounds }}</template>
|
<template #label>{{ i18n.ts.sounds }}</template>
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkFolder v-for="type in soundsKeys" :key="type">
|
<MkFolder v-for="type in operationTypes" :key="type">
|
||||||
<template #label>{{ i18n.t('_sfx.' + type) }}</template>
|
<template #label>{{ i18n.t('_sfx.' + type) }}</template>
|
||||||
<template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template>
|
<template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template>
|
||||||
|
|
||||||
<XSound :type="sounds[type].type" :volume="sounds[type].volume" @update="(res) => updated(type, res)"/>
|
<XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
@ -27,6 +33,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Ref, computed, ref } from 'vue';
|
import { Ref, computed, ref } from 'vue';
|
||||||
|
import type { SoundType, OperationType } from '@/scripts/sound.js';
|
||||||
|
import type { SoundStore } from '@/store.js';
|
||||||
import XSound from './sounds.sound.vue';
|
import XSound from './sounds.sound.vue';
|
||||||
import MkRange from '@/components/MkRange.vue';
|
import MkRange from '@/components/MkRange.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
@ -34,23 +42,39 @@ import FormSection from '@/components/form/section.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
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 { operationTypes } from '@/scripts/sound.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
|
||||||
|
const notUseSound = computed(defaultStore.makeGetterSetter('sound_notUseSound'));
|
||||||
|
const useSoundOnlyWhenActive = computed(defaultStore.makeGetterSetter('sound_useSoundOnlyWhenActive'));
|
||||||
const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume'));
|
const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume'));
|
||||||
|
|
||||||
const soundsKeys = ['note', 'noteMy', 'notification', 'antenna', 'channel'] as const;
|
const sounds = ref<Record<OperationType, Ref<SoundStore>>>({
|
||||||
|
|
||||||
const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({
|
|
||||||
note: defaultStore.reactiveState.sound_note,
|
note: defaultStore.reactiveState.sound_note,
|
||||||
noteMy: defaultStore.reactiveState.sound_noteMy,
|
noteMy: defaultStore.reactiveState.sound_noteMy,
|
||||||
notification: defaultStore.reactiveState.sound_notification,
|
notification: defaultStore.reactiveState.sound_notification,
|
||||||
antenna: defaultStore.reactiveState.sound_antenna,
|
antenna: defaultStore.reactiveState.sound_antenna,
|
||||||
channel: defaultStore.reactiveState.sound_channel,
|
channel: defaultStore.reactiveState.sound_channel,
|
||||||
|
reaction: defaultStore.reactiveState.sound_reaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getSoundTypeName(f: SoundType): string {
|
||||||
|
switch (f) {
|
||||||
|
case null:
|
||||||
|
return i18n.ts.none;
|
||||||
|
case '_driveFile_':
|
||||||
|
return i18n.ts._soundSettings.driveFile;
|
||||||
|
default:
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function updated(type: keyof typeof sounds.value, sound) {
|
async function updated(type: keyof typeof sounds.value, sound) {
|
||||||
const v = {
|
const v: SoundStore = {
|
||||||
type: sound.type,
|
type: sound.type,
|
||||||
|
fileId: sound.fileId,
|
||||||
|
fileUrl: sound.fileUrl,
|
||||||
volume: sound.volume,
|
volume: sound.volume,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkLoading v-if="fetching"/>
|
<MkLoading v-if="fetching"/>
|
||||||
<div v-if="!fetching && files.length > 0" :class="$style.stream">
|
<div v-if="!fetching && files.length > 0" :class="$style.stream">
|
||||||
<template v-for="file in files" :key="file.note.id + file.file.id">
|
<template v-for="file in files" :key="file.note.id + file.file.id">
|
||||||
<div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.sensitive" @click="showingFiles.push(file.file.id)">
|
<div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.img" @click="showingFiles.push(file.file.id)">
|
||||||
<div>
|
<!-- TODO: 画像以外のファイルに対応 -->
|
||||||
<div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div>
|
<ImgWithBlurhash :class="$style.sensitiveImg" :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name" :forceBlurhash="true"/>
|
||||||
<div>{{ i18n.ts.clickToShow }}</div>
|
<div :class="$style.sensitive">
|
||||||
|
<div>
|
||||||
|
<div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div>
|
||||||
|
<div>{{ i18n.ts.clickToShow }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkA v-else :class="$style.img" :to="notePage(file.note)">
|
<MkA v-else :class="$style.img" :to="notePage(file.note)">
|
||||||
|
@ -88,6 +92,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.img {
|
.img {
|
||||||
|
position: relative;
|
||||||
height: 128px;
|
height: 128px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
@ -99,8 +104,24 @@ onMounted(() => {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sensitiveImg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
filter: brightness(0.7);
|
||||||
|
}
|
||||||
.sensitive {
|
.sensitive {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<XPages v-else-if="tab === 'pages'" :user="user"/>
|
<XPages v-else-if="tab === 'pages'" :user="user"/>
|
||||||
<XFlashs v-else-if="tab === 'flashs'" :user="user"/>
|
<XFlashs v-else-if="tab === 'flashs'" :user="user"/>
|
||||||
<XGallery v-else-if="tab === 'gallery'" :user="user"/>
|
<XGallery v-else-if="tab === 'gallery'" :user="user"/>
|
||||||
|
<XRaw v-else-if="tab === 'raw'" :user="user"/>
|
||||||
</div>
|
</div>
|
||||||
<MkError v-else-if="error" @retry="fetchUser()"/>
|
<MkError v-else-if="error" @retry="fetchUser()"/>
|
||||||
<MkLoading v-else/>
|
<MkLoading v-else/>
|
||||||
|
@ -44,6 +45,7 @@ const XLists = defineAsyncComponent(() => import('./lists.vue'));
|
||||||
const XPages = defineAsyncComponent(() => import('./pages.vue'));
|
const XPages = defineAsyncComponent(() => import('./pages.vue'));
|
||||||
const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
|
const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
|
||||||
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
|
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
|
||||||
|
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
acct: string;
|
acct: string;
|
||||||
|
@ -112,6 +114,10 @@ const headerTabs = $computed(() => user ? [{
|
||||||
key: 'gallery',
|
key: 'gallery',
|
||||||
title: i18n.ts.gallery,
|
title: i18n.ts.gallery,
|
||||||
icon: 'ti ti-icons',
|
icon: 'ti ti-icons',
|
||||||
|
}, {
|
||||||
|
key: 'raw',
|
||||||
|
title: 'Raw',
|
||||||
|
icon: 'ti ti-code',
|
||||||
}] : []);
|
}] : []);
|
||||||
|
|
||||||
definePageMetadata(computed(() => user ? {
|
definePageMetadata(computed(() => user ? {
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkSpacer :contentMax="600" :marginMin="16" :marginMax="32">
|
||||||
|
<div class="_gaps_m">
|
||||||
|
<div :class="$style.userMInfoRoot">
|
||||||
|
<MkAvatar :class="$style.userMInfoAvatar" :user="user" indicator link preview/>
|
||||||
|
<div :class="$style.userMInfoMetaRoot">
|
||||||
|
<span :class="$style.userMInfoMetaName"><MkUserName :class="$style.userMInfoMetaName" :user="user"/></span>
|
||||||
|
<span :class="$style.userMInfoMetaSub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
|
||||||
|
<span :class="$style.userMInfoMetaState">
|
||||||
|
<span v-if="suspended" :class="$style.suspended">Suspended</span>
|
||||||
|
<span v-if="silenced" :class="$style.silenced">Silenced</span>
|
||||||
|
<span v-if="moderator" :class="$style.moderator">Moderator</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||||
|
<MkKeyValue :copy="user.id" oneline>
|
||||||
|
<template #key>ID</template>
|
||||||
|
<template #value><span class="_monospace">{{ user.id }}</span></template>
|
||||||
|
</MkKeyValue>
|
||||||
|
<MkKeyValue oneline>
|
||||||
|
<template #key>{{ i18n.ts.createdAt }}</template>
|
||||||
|
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
|
||||||
|
</MkKeyValue>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormSection>
|
||||||
|
<template #label>Raw</template>
|
||||||
|
<MkObjectView tall :value="user"></MkObjectView>
|
||||||
|
</FormSection>
|
||||||
|
</div>
|
||||||
|
</MkSpacer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { acct } from '@/filters/user.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||||
|
import FormSection from '@/components/form/section.vue';
|
||||||
|
import MkObjectView from '@/components/MkObjectView.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
user: Misskey.entities.User;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const moderator = computed(() => props.user.isModerator ?? false);
|
||||||
|
const silenced = computed(() => props.user.isSilenced ?? false);
|
||||||
|
const suspended = computed(() => props.user.isSuspended ?? false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.userMInfoRoot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userMInfoAvatar {
|
||||||
|
display: block;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userMInfoMetaRoot {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userMInfoMetaName {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userMInfoMetaSub {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 85%;
|
||||||
|
opacity: 0.7;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userMInfoMetaState {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .suspended,
|
||||||
|
> .silenced,
|
||||||
|
> .moderator {
|
||||||
|
display: inline-block;
|
||||||
|
border: solid 1px;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .suspended {
|
||||||
|
color: var(--error);
|
||||||
|
border-color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .silenced {
|
||||||
|
color: var(--warn);
|
||||||
|
border-color: var(--warn);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .moderator {
|
||||||
|
color: var(--success);
|
||||||
|
border-color: var(--success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,9 @@
|
||||||
|
export default async function hasAudio(media: HTMLMediaElement) {
|
||||||
|
const cloned = media.cloneNode() as HTMLMediaElement;
|
||||||
|
cloned.muted = (cloned as typeof cloned & Partial<HTMLVideoElement>).playsInline = true;
|
||||||
|
cloned.play();
|
||||||
|
await new Promise((resolve) => cloned.addEventListener('playing', resolve));
|
||||||
|
const result = !!(cloned as any).audioTracks?.length || (cloned as any).mozHasAudio || !!(cloned as any).webkitAudioDecodedByteCount;
|
||||||
|
cloned.remove();
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -3,13 +3,22 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { SoundStore } from '@/store.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
const ctx = new AudioContext();
|
let ctx: AudioContext;
|
||||||
const cache = new Map<string, AudioBuffer>();
|
const cache = new Map<string, AudioBuffer>();
|
||||||
|
let canPlay = true;
|
||||||
|
|
||||||
export const soundsTypes = [
|
export const soundsTypes = [
|
||||||
|
// 音声なし
|
||||||
null,
|
null,
|
||||||
|
|
||||||
|
// ドライブの音声
|
||||||
|
'_driveFile_',
|
||||||
|
|
||||||
|
// プリインストール
|
||||||
'syuilo/n-aec',
|
'syuilo/n-aec',
|
||||||
'syuilo/n-aec-4va',
|
'syuilo/n-aec-4va',
|
||||||
'syuilo/n-aec-4vb',
|
'syuilo/n-aec-4vb',
|
||||||
|
@ -38,6 +47,8 @@ export const soundsTypes = [
|
||||||
'syuilo/waon',
|
'syuilo/waon',
|
||||||
'syuilo/popo',
|
'syuilo/popo',
|
||||||
'syuilo/triple',
|
'syuilo/triple',
|
||||||
|
'syuilo/bubble1',
|
||||||
|
'syuilo/bubble2',
|
||||||
'syuilo/poi1',
|
'syuilo/poi1',
|
||||||
'syuilo/poi2',
|
'syuilo/poi2',
|
||||||
'syuilo/pirori',
|
'syuilo/pirori',
|
||||||
|
@ -61,37 +72,116 @@ export const soundsTypes = [
|
||||||
'noizenecio/kick_gaba7',
|
'noizenecio/kick_gaba7',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export async function loadAudio(file: string, useCache = true) {
|
export const operationTypes = [
|
||||||
if (useCache && cache.has(file)) {
|
'noteMy',
|
||||||
return cache.get(file)!;
|
'note',
|
||||||
|
'antenna',
|
||||||
|
'channel',
|
||||||
|
'notification',
|
||||||
|
'reaction',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** サウンドの種類 */
|
||||||
|
export type SoundType = typeof soundsTypes[number];
|
||||||
|
|
||||||
|
/** スプライトの種類 */
|
||||||
|
export type OperationType = typeof operationTypes[number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音声を読み込む
|
||||||
|
* @param soundStore サウンド設定
|
||||||
|
* @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする
|
||||||
|
*/
|
||||||
|
export async function loadAudio(soundStore: SoundStore, options?: { useCache?: boolean; }) {
|
||||||
|
if (_DEV_) console.log('loading audio. opts:', options);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
if (ctx == null) {
|
||||||
|
ctx = new AudioContext();
|
||||||
|
}
|
||||||
|
if (options?.useCache ?? true) {
|
||||||
|
if (soundStore.type === '_driveFile_' && cache.has(soundStore.fileId)) {
|
||||||
|
if (_DEV_) console.log('use cache');
|
||||||
|
return cache.get(soundStore.fileId) as AudioBuffer;
|
||||||
|
} else if (cache.has(soundStore.type)) {
|
||||||
|
if (_DEV_) console.log('use cache');
|
||||||
|
return cache.get(soundStore.type) as AudioBuffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: Response;
|
||||||
|
|
||||||
|
if (soundStore.type === '_driveFile_') {
|
||||||
|
try {
|
||||||
|
response = await fetch(soundStore.fileUrl);
|
||||||
|
} catch (err) {
|
||||||
|
try {
|
||||||
|
// URLが変わっている可能性があるのでドライブ側からURLを取得するフォールバック
|
||||||
|
const apiRes = await os.api('drive/files/show', {
|
||||||
|
fileId: soundStore.fileId,
|
||||||
|
});
|
||||||
|
response = await fetch(apiRes.url);
|
||||||
|
} catch (fbErr) {
|
||||||
|
// それでも無理なら諦める
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
response = await fetch(`/client-assets/sounds/${soundStore.type}.mp3`);
|
||||||
|
} catch (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`/client-assets/sounds/${file}.mp3`);
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
|
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
|
||||||
|
|
||||||
if (useCache) {
|
if (options?.useCache ?? true) {
|
||||||
cache.set(file, audioBuffer);
|
if (soundStore.type === '_driveFile_') {
|
||||||
|
cache.set(soundStore.fileId, audioBuffer);
|
||||||
|
} else {
|
||||||
|
cache.set(soundStore.type, audioBuffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return audioBuffer;
|
return audioBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') {
|
/**
|
||||||
const sound = defaultStore.state[`sound_${type}`];
|
* 既定のスプライトを再生する
|
||||||
if (_DEV_) console.log('play', type, sound);
|
* @param type スプライトの種類を指定
|
||||||
if (sound.type == null) return;
|
*/
|
||||||
playFile(sound.type, sound.volume);
|
export function play(operationType: OperationType) {
|
||||||
|
const sound = defaultStore.state[`sound_${operationType}`];
|
||||||
|
if (_DEV_) console.log('play', operationType, sound);
|
||||||
|
if (sound.type == null || !canPlay) return;
|
||||||
|
|
||||||
|
canPlay = false;
|
||||||
|
playFile(sound).then(() => {
|
||||||
|
// ごく短時間に音が重複しないように
|
||||||
|
setTimeout(() => {
|
||||||
|
canPlay = true;
|
||||||
|
}, 25);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function playFile(file: string, volume: number) {
|
/**
|
||||||
const buffer = await loadAudio(file);
|
* サウンド設定形式で指定された音声を再生する
|
||||||
createSourceNode(buffer, volume)?.start();
|
* @param soundStore サウンド設定
|
||||||
|
*/
|
||||||
|
export async function playFile(soundStore: SoundStore) {
|
||||||
|
const buffer = await loadAudio(soundStore);
|
||||||
|
if (!buffer) return;
|
||||||
|
createSourceNode(buffer, soundStore.volume)?.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null {
|
export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null {
|
||||||
const masterVolume = defaultStore.state.sound_masterVolume;
|
const masterVolume = defaultStore.state.sound_masterVolume;
|
||||||
if (masterVolume === 0 || volume === 0) {
|
if (isMute() || masterVolume === 0 || volume === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,3 +194,39 @@ export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBuf
|
||||||
|
|
||||||
return soundSource;
|
return soundSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音声の長さをミリ秒で取得する
|
||||||
|
* @param file ファイルのURL(ドライブIDではない)
|
||||||
|
*/
|
||||||
|
export async function getSoundDuration(file: string): Promise<number> {
|
||||||
|
const audioEl = document.createElement('audio');
|
||||||
|
audioEl.src = file;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const si = setInterval(() => {
|
||||||
|
if (audioEl.readyState > 0) {
|
||||||
|
resolve(audioEl.duration * 1000);
|
||||||
|
clearInterval(si);
|
||||||
|
audioEl.remove();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ミュートすべきかどうかを判断する
|
||||||
|
*/
|
||||||
|
export function isMute(): boolean {
|
||||||
|
if (defaultStore.state.sound_notUseSound) {
|
||||||
|
// サウンドを出力しない
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// noinspection RedundantIfStatementJS
|
||||||
|
if (defaultStore.state.sound_useSoundOnlyWhenActive && document.visibilityState === 'hidden') {
|
||||||
|
// ブラウザがアクティブな時のみサウンドを出力する
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import { markRaw, ref } from 'vue';
|
import { markRaw, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { miLocalStorage } from './local-storage.js';
|
import { miLocalStorage } from './local-storage.js';
|
||||||
|
import type { SoundType } from '@/scripts/sound.js';
|
||||||
import { Storage } from '@/pizzax.js';
|
import { Storage } from '@/pizzax.js';
|
||||||
|
|
||||||
interface PostFormAction {
|
interface PostFormAction {
|
||||||
|
@ -35,6 +36,22 @@ interface PageViewInterruptor {
|
||||||
handler: (page: Misskey.entities.Page) => unknown;
|
handler: (page: Misskey.entities.Page) => unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** サウンド設定 */
|
||||||
|
export type SoundStore = {
|
||||||
|
type: Exclude<SoundType, '_driveFile_'>;
|
||||||
|
volume: number;
|
||||||
|
} | {
|
||||||
|
type: '_driveFile_';
|
||||||
|
|
||||||
|
/** ドライブのファイルID */
|
||||||
|
fileId: string;
|
||||||
|
|
||||||
|
/** ファイルURL(こちらが優先される) */
|
||||||
|
fileUrl: string;
|
||||||
|
|
||||||
|
volume: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const postFormActions: PostFormAction[] = [];
|
export const postFormActions: PostFormAction[] = [];
|
||||||
export const userActions: UserAction[] = [];
|
export const userActions: UserAction[] = [];
|
||||||
export const noteActions: NoteAction[] = [];
|
export const noteActions: NoteAction[] = [];
|
||||||
|
@ -330,6 +347,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: 'medium' as 'small' | 'medium' | 'large',
|
default: 'medium' as 'small' | 'medium' | 'large',
|
||||||
},
|
},
|
||||||
|
limitWidthOfReaction: {
|
||||||
|
where: 'device',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
forceShowAds: {
|
forceShowAds: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -387,25 +408,37 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: 0.3,
|
default: 0.3,
|
||||||
},
|
},
|
||||||
|
sound_notUseSound: {
|
||||||
|
where: 'device',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
sound_useSoundOnlyWhenActive: {
|
||||||
|
where: 'device',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
sound_note: {
|
sound_note: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: { type: 'syuilo/n-aec', volume: 1 },
|
default: { type: 'syuilo/n-aec', volume: 1 } as SoundStore,
|
||||||
},
|
},
|
||||||
sound_noteMy: {
|
sound_noteMy: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: { type: 'syuilo/n-cea-4va', volume: 1 },
|
default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore,
|
||||||
},
|
},
|
||||||
sound_notification: {
|
sound_notification: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: { type: 'syuilo/n-ea', volume: 1 },
|
default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
|
||||||
},
|
},
|
||||||
sound_antenna: {
|
sound_antenna: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: { type: 'syuilo/triple', volume: 1 },
|
default: { type: 'syuilo/triple', volume: 1 } as SoundStore,
|
||||||
},
|
},
|
||||||
sound_channel: {
|
sound_channel: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: { type: 'syuilo/square-pico', volume: 1 },
|
default: { type: 'syuilo/square-pico', volume: 1 } as SoundStore,
|
||||||
|
},
|
||||||
|
sound_reaction: {
|
||||||
|
where: 'device',
|
||||||
|
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ import { useStream } from '@/stream.js';
|
||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
const name = 'jobQueue';
|
const name = 'jobQueue';
|
||||||
|
|
||||||
|
@ -102,7 +103,15 @@ const prev = reactive({} as typeof current);
|
||||||
let jammedAudioBuffer: AudioBuffer | null = $ref(null);
|
let jammedAudioBuffer: AudioBuffer | null = $ref(null);
|
||||||
let jammedSoundNodePlaying: boolean = $ref(false);
|
let jammedSoundNodePlaying: boolean = $ref(false);
|
||||||
|
|
||||||
sound.loadAudio('syuilo/queue-jammed').then(buf => jammedAudioBuffer = buf);
|
if (defaultStore.state.sound_masterVolume) {
|
||||||
|
sound.loadAudio({
|
||||||
|
type: 'syuilo/queue-jammed',
|
||||||
|
volume: 1,
|
||||||
|
}).then(buf => {
|
||||||
|
if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer');
|
||||||
|
jammedAudioBuffer = buf;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (const domain of ['inbox', 'deliver']) {
|
for (const domain of ['inbox', 'deliver']) {
|
||||||
prev[domain] = deepClone(current[domain]);
|
prev[domain] = deepClone(current[domain]);
|
||||||
|
|
|
@ -25,3 +25,21 @@ vi.mock('@/store.js', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add mocks for Web Audio API
|
||||||
|
const AudioNodeMock = vi.fn(() => ({
|
||||||
|
connect: vi.fn(() => ({ connect: vi.fn() })),
|
||||||
|
start: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const GainNodeMock = vi.fn(() => ({
|
||||||
|
gain: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const AudioContextMock = vi.fn(() => ({
|
||||||
|
createBufferSource: vi.fn(() => new AudioNodeMock()),
|
||||||
|
createGain: vi.fn(() => new GainNodeMock()),
|
||||||
|
decodeAudioData: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.stubGlobal('AudioContext', AudioContextMock);
|
||||||
|
|
|
@ -1565,7 +1565,8 @@ export type Endpoints = {
|
||||||
injectFeaturedNote?: boolean;
|
injectFeaturedNote?: boolean;
|
||||||
receiveAnnouncementEmail?: boolean;
|
receiveAnnouncementEmail?: boolean;
|
||||||
alwaysMarkNsfw?: boolean;
|
alwaysMarkNsfw?: boolean;
|
||||||
mutedWords?: string[][];
|
mutedWords?: (string[] | string)[];
|
||||||
|
hardMutedWords?: (string[] | string)[];
|
||||||
notificationRecieveConfig?: any;
|
notificationRecieveConfig?: any;
|
||||||
emailNotificationTypes?: string[];
|
emailNotificationTypes?: string[];
|
||||||
alsoKnownAs?: string[];
|
alsoKnownAs?: string[];
|
||||||
|
@ -2534,7 +2535,8 @@ type MeDetailed = UserDetailed & {
|
||||||
integrations: Record<string, any>;
|
integrations: Record<string, any>;
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
isExplorable: boolean;
|
isExplorable: boolean;
|
||||||
mutedWords: string[][];
|
mutedWords: (string[] | string)[];
|
||||||
|
hardMutedWords: (string[] | string)[];
|
||||||
notificationRecieveConfig: {
|
notificationRecieveConfig: {
|
||||||
[notificationType in typeof notificationTypes_2[number]]?: {
|
[notificationType in typeof notificationTypes_2[number]]?: {
|
||||||
type: 'all';
|
type: 'all';
|
||||||
|
@ -3071,9 +3073,9 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
|
||||||
//
|
//
|
||||||
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:20:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:20:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:644:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:645:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||||
// src/entities.ts:116:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
// src/entities.ts:117:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
||||||
// src/entities.ts:627:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
// src/entities.ts:628:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||||
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
||||||
|
|
||||||
// (No @packageDocumentation comment for this package)
|
// (No @packageDocumentation comment for this package)
|
||||||
|
|
|
@ -22,11 +22,11 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/api-extractor": "7.38.3",
|
"@microsoft/api-extractor": "7.38.3",
|
||||||
"@swc/jest": "0.2.29",
|
"@swc/jest": "0.2.29",
|
||||||
"@types/jest": "29.5.8",
|
"@types/jest": "29.5.10",
|
||||||
"@types/node": "20.9.1",
|
"@types/node": "20.10.0",
|
||||||
"@typescript-eslint/eslint-plugin": "6.11.0",
|
"@typescript-eslint/eslint-plugin": "6.12.0",
|
||||||
"@typescript-eslint/parser": "6.11.0",
|
"@typescript-eslint/parser": "6.12.0",
|
||||||
"eslint": "8.53.0",
|
"eslint": "8.54.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-fetch-mock": "3.0.3",
|
"jest-fetch-mock": "3.0.3",
|
||||||
"jest-websocket-mock": "2.5.0",
|
"jest-websocket-mock": "2.5.0",
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/cli": "0.1.63",
|
"@swc/cli": "0.1.63",
|
||||||
"@swc/core": "1.3.96",
|
"@swc/core": "1.3.99",
|
||||||
"eventemitter3": "5.0.1",
|
"eventemitter3": "5.0.1",
|
||||||
"reconnecting-websocket": "4.4.0"
|
"reconnecting-websocket": "4.4.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -432,7 +432,8 @@ export type Endpoints = {
|
||||||
injectFeaturedNote?: boolean;
|
injectFeaturedNote?: boolean;
|
||||||
receiveAnnouncementEmail?: boolean;
|
receiveAnnouncementEmail?: boolean;
|
||||||
alwaysMarkNsfw?: boolean;
|
alwaysMarkNsfw?: boolean;
|
||||||
mutedWords?: string[][];
|
mutedWords?: (string[] | string)[];
|
||||||
|
hardMutedWords?: (string[] | string)[];
|
||||||
notificationRecieveConfig?: any;
|
notificationRecieveConfig?: any;
|
||||||
emailNotificationTypes?: string[];
|
emailNotificationTypes?: string[];
|
||||||
alsoKnownAs?: string[];
|
alsoKnownAs?: string[];
|
||||||
|
|
|
@ -112,7 +112,8 @@ export type MeDetailed = UserDetailed & {
|
||||||
integrations: Record<string, any>;
|
integrations: Record<string, any>;
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
isExplorable: boolean;
|
isExplorable: boolean;
|
||||||
mutedWords: string[][];
|
mutedWords: (string[] | string)[];
|
||||||
|
hardMutedWords: (string[] | string)[];
|
||||||
notificationRecieveConfig: {
|
notificationRecieveConfig: {
|
||||||
[notificationType in typeof notificationTypes[number]]?: {
|
[notificationType in typeof notificationTypes[number]]?: {
|
||||||
type: 'all';
|
type: 'all';
|
||||||
|
|
|
@ -9,14 +9,14 @@
|
||||||
"lint": "pnpm typecheck && pnpm eslint"
|
"lint": "pnpm typecheck && pnpm eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "0.19.5",
|
"esbuild": "0.19.8",
|
||||||
"idb-keyval": "6.2.1",
|
"idb-keyval": "6.2.1",
|
||||||
"misskey-js": "workspace:*"
|
"misskey-js": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/parser": "6.11.0",
|
"@typescript-eslint/parser": "6.12.0",
|
||||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
||||||
"eslint": "8.53.0",
|
"eslint": "8.54.0",
|
||||||
"eslint-plugin-import": "2.29.0",
|
"eslint-plugin-import": "2.29.0",
|
||||||
"typescript": "5.3.2"
|
"typescript": "5.3.2"
|
||||||
},
|
},
|
||||||
|
|
1444
pnpm-lock.yaml
1444
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue