Merge branch 'develop' into feature/emoji-grid

This commit is contained in:
おさむのひと 2024-07-27 08:18:04 +09:00 committed by GitHub
commit 7e0343d724
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
129 changed files with 1391 additions and 687 deletions

View file

@ -1,5 +1,11 @@
# misskey settings
# MISSKEY_URL=https://example.tld/
# db settings # db settings
POSTGRES_PASSWORD=example-misskey-pass POSTGRES_PASSWORD=example-misskey-pass
# DATABASE_PASSWORD=${POSTGRES_PASSWORD}
POSTGRES_USER=example-misskey-user POSTGRES_USER=example-misskey-user
# DATABASE_USER=${POSTGRES_USER}
POSTGRES_DB=misskey POSTGRES_DB=misskey
# DATABASE_DB=${POSTGRES_DB}
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}" DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"

View file

@ -6,6 +6,7 @@
#───┘ URL └───────────────────────────────────────────────────── #───┘ URL └─────────────────────────────────────────────────────
# Final accessible URL seen by a user. # Final accessible URL seen by a user.
# You can set url from an environment variable instead.
url: https://example.tld/ url: https://example.tld/
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
@ -38,9 +39,11 @@ db:
port: 5432 port: 5432
# Database name # Database name
# You can set db from an environment variable instead.
db: misskey db: misskey
# Auth # Auth
# You can set user and pass from environment variables instead.
user: example-misskey-user user: example-misskey-user
pass: example-misskey-pass pass: example-misskey-pass

View file

@ -12,7 +12,6 @@ node_modules/
packages/*/node_modules packages/*/node_modules
redis/ redis/
files/ files/
misskey-assets/
fluent-emojis/ fluent-emojis/
.pnp.* .pnp.*

View file

@ -53,8 +53,8 @@ body:
Examples: Examples:
* Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4 * Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4
* Browser: Chrome 113.0.5672.126 * Browser: Chrome 113.0.5672.126
* Server URL: misskey.io * Server URL: misskey.example.com
* Misskey: 13.x.x * Misskey: 2024.x.x
value: | value: |
* Model and OS of the device(s): * Model and OS of the device(s):
* Browser: * Browser:
@ -74,11 +74,11 @@ body:
Examples: Examples:
* Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment * Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment
* Misskey: 13.x.x * Misskey: 2024.x.x
* Node: 20.x.x * Node: 20.x.x
* PostgreSQL: 15.x.x * PostgreSQL: 15.x.x
* Redis: 7.x.x * Redis: 7.x.x
* OS and Architecture: Ubuntu 22.04.2 LTS aarch64 * OS and Architecture: Ubuntu 24.04.2 LTS aarch64
value: | value: |
* Installation Method or Hosting Service: * Installation Method or Hosting Service:
* Misskey: * Misskey:

View file

@ -21,7 +21,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View file

@ -14,7 +14,7 @@ jobs:
- name: Checkout head - name: Checkout head
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'

View file

@ -28,7 +28,7 @@ jobs:
- name: setup node - name: setup node
id: setup-node id: setup-node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: pnpm cache: pnpm

View file

@ -34,7 +34,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -29,7 +29,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2 - uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -40,6 +40,8 @@ jobs:
needs: [pnpm_install] needs: [pnpm_install]
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
env:
eslint-cache-version: v1
strategy: strategy:
matrix: matrix:
workspace: workspace:
@ -53,13 +55,20 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2 - uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
- run: corepack enable - run: corepack enable
- run: pnpm i --frozen-lockfile - run: pnpm i --frozen-lockfile
- run: pnpm --filter ${{ matrix.workspace }} run eslint - name: Restore eslint cache
uses: actions/cache@v4.0.2
with:
path: node_modules/.cache/eslint
key: eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-
- run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location node_modules/.cache/eslint --cache-strategy content
typecheck: typecheck:
needs: [pnpm_install] needs: [pnpm_install]
@ -76,7 +85,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2 - uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View file

@ -19,7 +19,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2 - uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View file

@ -26,7 +26,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -36,7 +36,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js 20.x - name: Use Node.js 20.x
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View file

@ -46,7 +46,7 @@ jobs:
- name: Install FFmpeg - name: Install FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3 uses: FedericoCarboni/setup-ffmpeg@v3
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@ -93,7 +93,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -35,7 +35,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@ -90,7 +90,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -31,7 +31,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }} - name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -25,7 +25,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -27,7 +27,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

1
.gitignore vendored
View file

@ -59,6 +59,7 @@ ormconfig.json
temp temp
/packages/frontend/src/**/*.stories.ts /packages/frontend/src/**/*.stories.ts
tsdoc-metadata.json tsdoc-metadata.json
misskey-assets
# blender backups # blender backups
*.blend1 *.blend1

3
.gitmodules vendored
View file

@ -1,6 +1,3 @@
[submodule "misskey-assets"]
path = misskey-assets
url = https://github.com/misskey-dev/assets.git
[submodule "fluent-emojis"] [submodule "fluent-emojis"]
path = fluent-emojis path = fluent-emojis
url = https://github.com/misskey-dev/emojis.git url = https://github.com/misskey-dev/emojis.git

View file

@ -1,7 +1,8 @@
## Unreleased ## 2024.7.0
### Note ### Note
- デッキUIの新着ートをサウンドで通知する機能の追加v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。 - デッキUIの新着ートをサウンドで通知する機能の追加v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
- Streaming APIにて入力が不正な場合にはそのメッセージを無視するようになりました。 #14251
### General ### General
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705 - Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
@ -21,6 +22,9 @@
(Based on https://github.com/taiyme/misskey/pull/226) (Based on https://github.com/taiyme/misskey/pull/226)
- Enhance: サーバー情報ページ・お問い合わせページを改善 - Enhance: サーバー情報ページ・お問い合わせページを改善
(Cherry-picked from https://github.com/taiyme/misskey/pull/238) (Cherry-picked from https://github.com/taiyme/misskey/pull/238)
- Enhance: AiScriptを0.19.0にアップデート
- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
- Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正 - Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968) - Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
- Fix: リバーシの対局を正しく共有できないことがある問題を修正 - Fix: リバーシの対局を正しく共有できないことがある問題を修正
@ -29,6 +33,16 @@
- Fix: テーマプレビューが見れない問題を修正 - Fix: テーマプレビューが見れない問題を修正
- Fix: ショートカットキーが連打できる問題を修正 - Fix: ショートカットキーが連打できる問題を修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/234) (Cherry-picked from https://github.com/taiyme/misskey/pull/234)
- Fix: MkSignin.vueのcredentialRequestからReactivityを削除ProxyがPasskey認証処理に渡ることを避けるため
- Fix: 「アニメーション画像を再生しない」がオンのときでもサーバーのバナー画像・背景画像がアニメーションしてしまう問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/574)
- Fix: Twitchの埋め込みが開けない問題を修正
- Fix: 子メニューの高さがウィンドウからはみ出ることがある問題を修正
- Fix: 個人宛てのダイアログ形式のお知らせが即時表示されない問題を修正
- Fix: 一部の画像がセンシティブ指定されているときに画面に何も表示されないことがあるのを修正
- Fix: リアクションしたユーザー一覧のユーザー名がはみ出る問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/672)
- Fix: `/share`ページにおいて絵文字ピッカーを開くことができない問題を修正
### Server ### Server
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
@ -38,7 +52,8 @@
- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに - Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに - Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに - Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
- Fix: チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正 - Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように
- Fix: チャート生成時にinstance.suspensionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006) - Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036) - Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059) - Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
@ -53,8 +68,18 @@
2. フォロー中かつ非アクティブなユーザ 2. フォロー中かつ非アクティブなユーザ
3. フォローしていないアクティブなユーザ 3. フォローしていないアクティブなユーザ
4. フォローしていない非アクティブなユーザ 4. フォローしていない非アクティブなユーザ
また、自分自身のアカウントもサジェストされるようになりました。
- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正 - Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652) (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652)
- Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正
- Fix: FTT有効時にリモートユーザーのートがHTLにキャッシュされる問題を修正
- Fix: 一部の通知がローカル上のリモートユーザーに対して行われていた問題を修正
- Fix: エラーメッセージの誤字を修正 (#14213)
- Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正
- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正
(Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1)
- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251
### Misskey.js ### Misskey.js
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応) - Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)

View file

@ -1,7 +1,7 @@
# Contribution guide # Contribution guide
We're glad you're interested in contributing Misskey! In this document you will find the information you need to contribute to the project. We're glad you're interested in contributing Misskey! In this document you will find the information you need to contribute to the project.
> **Note** > [!NOTE]
> This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.** > This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.**
> Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\ > Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\
> The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. > The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language.
@ -17,16 +17,31 @@ Before creating an issue, please check the following:
- Issues should only be used to feature requests, suggestions, and bug tracking. - Issues should only be used to feature requests, suggestions, and bug tracking.
- Please ask questions or troubleshooting in [GitHub Discussions](https://github.com/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3). - Please ask questions or troubleshooting in [GitHub Discussions](https://github.com/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3).
> **Warning** > [!WARNING]
> Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged. > Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
## Before implementation ### Recommended discussing before implementation
We welcome your proposal.
When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented. When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented.
At this point, you also need to clarify the goals of the PR you will create, and make sure that the other members of the team are aware of them. At this point, you also need to clarify the goals of the PR you will create, and make sure that the other members of the team are aware of them.
PRs that do not have a clear set of do's and don'ts tend to be bloated and difficult to review. PRs that do not have a clear set of do's and don'ts tend to be bloated and difficult to review.
Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work. Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask Committer to assign you).
By expressing your intention to work on the Issue, you can prevent conflicts in the work.
To the Committers: you should not assign someone on it before the Final Decision.
### How issues are triaged
The Committers may:
* close an issue that is not reproducible on latest stable release,
* merge an issue into another issue,
* split an issue into multiple issues,
* or re-open that has been closed for some reason which is not applicable anymore.
@syuilo reserves the Final Decision rights including whether the project will implement feature and how to implement, these rights are not always exercised.
## Well-known branches ## Well-known branches
- **`master`** branch is tracking the latest release and used for production purposes. - **`master`** branch is tracking the latest release and used for production purposes.
@ -37,14 +52,14 @@ Also, when you start implementation, assign yourself to the Issue (if you cannot
## Creating a PR ## Creating a PR
Thank you for your PR! Before creating a PR, please check the following: Thank you for your PR! Before creating a PR, please check the following:
- If possible, prefix the title with a keyword that identifies the type of this PR, as shown below. - If possible, prefix the title with a keyword that identifies the type of this PR, as shown below.
- `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc - `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc
- Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR. - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR.
- If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text. - If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text.
- Please add the summary of the changes to [`CHANGELOG.md`](/CHANGELOG.md). However, this is not necessary for changes that do not affect the users, such as refactoring. - Please add the summary of the changes to [`CHANGELOG.md`](/CHANGELOG.md). However, this is not necessary for changes that do not affect the users, such as refactoring.
- Check if there are any documents that need to be created or updated due to this change. - Check if there are any documents that need to be created or updated due to this change.
- If you have added a feature or fixed a bug, please add a test case if possible. - If you have added a feature or fixed a bug, please add a test case if possible.
- Please make sure that tests and Lint are passed in advance. - Please make sure that tests and Lint are passed in advance.
- You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing) - You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing)
- If this PR includes UI changes, please attach a screenshot in the text. - If this PR includes UI changes, please attach a screenshot in the text.
Thanks for your cooperation 🤗 Thanks for your cooperation 🤗
@ -54,8 +69,8 @@ Be willing to comment on the good points and not just the things you want fixed
### Review perspective ### Review perspective
- Scope - Scope
- Are the goals of the PR clear? - Are the goals of the PR clear?
- Is the granularity of the PR appropriate? - Is the granularity of the PR appropriate?
- Security - Security
- Does merging this PR create a vulnerability? - Does merging this PR create a vulnerability?
- Performance - Performance
@ -77,7 +92,7 @@ An actual domain will be assigned so you can test the federation.
## Release ## Release
### Release Instructions ### Release Instructions
1. Commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json)) 1. Commit version changes in the `develop` branch ([package.json](package.json))
2. Create a release PR. 2. Create a release PR.
- Into `master` from `develop` branch. - Into `master` from `develop` branch.
- The title must be in the format `Release: x.y.z`. - The title must be in the format `Release: x.y.z`.
@ -88,7 +103,7 @@ An actual domain will be assigned so you can test the federation.
- The target branch must be `master` - The target branch must be `master`
- The tag name must be the version - The tag name must be the version
> **Note** > [!NOTE]
> Why this instruction is necessary: > Why this instruction is necessary:
> - To perform final QA checks > - To perform final QA checks
> - To distribute responsibility > - To distribute responsibility
@ -106,12 +121,42 @@ If your language is not listed in Crowdin, please open an issue.
![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg) ![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)
## Development ## Development
During development, it is useful to use the ### Setup
Before developing, you have to set up environment. Misskey requires Redis, PostgreSQL, and FFmpeg.
You would want to install Meilisearch to experiment related features. Technically, meilisearch is not strict requirement, but some features and tests require it.
There are a few ways to proceed.
#### Use system-wide software
You could install them in system-wide (such as from package manager).
#### Use `docker compose`
You could obtain middleware container by typing `docker compose -f $PROJECT_ROOT/compose.local-db.yml up -d`.
#### Use Devcontainer
Devcontainer also has necessary setting. This method can be done by connecting from VSCode.
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.
To use Dev Container, open the project directory on VSCode with Dev Containers installed.
**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled.
It will run the following command automatically inside the container.
``` bash
git submodule update --init
pnpm install --frozen-lockfile
cp .devcontainer/devcontainer.yml .config/default.yml
pnpm build
pnpm migrate
```
After finishing the migration, you can proceed.
### Start developing
During development, it is useful to use the
``` ```
pnpm dev pnpm dev
``` ```
command. command.
- Server-side source files and automatically builds them if they are modified. Automatically start the server process(es). - Server-side source files and automatically builds them if they are modified. Automatically start the server process(es).
@ -135,26 +180,6 @@ MK_DEV_PREFER=backend pnpm dev
- To change the port of Vite, specify with `VITE_PORT` environment variable. - To change the port of Vite, specify with `VITE_PORT` environment variable.
- HMR may not work in some environments such as Windows. - HMR may not work in some environments such as Windows.
### Dev Container
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.
To use Dev Container, open the project directory on VSCode with Dev Containers installed.
**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled.
It will run the following command automatically inside the container.
``` bash
git submodule update --init
pnpm install --frozen-lockfile
cp .devcontainer/devcontainer.yml .config/default.yml
pnpm build
pnpm migrate
```
After finishing the migration, run the `pnpm dev` command to start the development server.
``` bash
pnpm dev
```
## Testing ## Testing
- Test codes are located in [`/packages/backend/test`](/packages/backend/test). - Test codes are located in [`/packages/backend/test`](/packages/backend/test).
@ -204,7 +229,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
### ルート定義 ### ルート定義
ルート定義は、以下の形式のオブジェクトの配列です。 ルート定義は、以下の形式のオブジェクトの配列です。
``` ts ```ts
{ {
name?: string; name?: string;
path: string; path: string;
@ -217,7 +242,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
} }
``` ```
> **Warning** > [!WARNING]
> 現状、ルートは定義された順に評価されます。 > 現状、ルートは定義された順に評価されます。
> たとえば、`/foo/:id`ルート定義の次に`/foo/bar`ルート定義がされていた場合、後者がマッチすることはありません。 > たとえば、`/foo/:id`ルート定義の次に`/foo/bar`ルート定義がされていた場合、後者がマッチすることはありません。
@ -279,7 +304,7 @@ export const Default = {
parameters: { parameters: {
layout: 'centered', layout: 'centered',
}, },
} satisfies StoryObj<typeof MkAvatar>; } satisfies StoryObj<typeof MyComponent>;
``` ```
If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file. If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file.
@ -390,7 +415,7 @@ describe('test', () => {
}) })
.useMocker(... .useMocker(...
.compile(); .compile();
fooService = app.get<FooService>(FooService); fooService = app.get<FooService>(FooService);
barService = app.get<BarService>(BarService) as jest.Mocked<BarService>; barService = app.get<BarService>(BarService) as jest.Mocked<BarService>;
@ -511,13 +536,13 @@ pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
- 作成されたスクリプトは不必要な変更を含むため除去してください - 作成されたスクリプトは不必要な変更を含むため除去してください
### JSON SchemaのobjectでanyOfを使うとき ### JSON SchemaのobjectでanyOfを使うとき
JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。 JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。
バリデーションが効かないため。SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます バリデーションが効かないため。SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます
https://github.com/misskey-dev/misskey/pull/10082 https://github.com/misskey-dev/misskey/pull/10082
テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合: テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合:
``` ```ts
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {

View file

@ -17,6 +17,8 @@ services:
networks: networks:
- internal_network - internal_network
- external_network - external_network
# env_file:
# - .config/docker.env
volumes: volumes:
- ./files:/misskey/files - ./files:/misskey/files
- ./.config:/misskey/.config:ro - ./.config:/misskey/.config:ro

8
locales/index.d.ts vendored
View file

@ -5012,6 +5012,14 @@ export interface Locale extends ILocale {
* *
*/ */
"tryAgain": string; "tryAgain": string;
/**
*
*/
"confirmWhenRevealingSensitiveMedia": string;
/**
*
*/
"sensitiveMediaRevealConfirm": string;
"_delivery": { "_delivery": {
/** /**
* *

View file

@ -1249,6 +1249,8 @@ noDescription: "説明文はありません"
alwaysConfirmFollow: "フォローの際常に確認する" alwaysConfirmFollow: "フォローの際常に確認する"
inquiry: "お問い合わせ" inquiry: "お問い合わせ"
tryAgain: "もう一度お試しください。" tryAgain: "もう一度お試しください。"
confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
_delivery: _delivery:
status: "配信状態" status: "配信状態"

@ -1 +0,0 @@
Subproject commit 0179793ec891856d6f37a3be16ba4c22f67a81b5

View file

@ -1,12 +1,12 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.5.0", "version": "2024.7.0-beta.2",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/misskey-dev/misskey.git" "url": "https://github.com/misskey-dev/misskey.git"
}, },
"packageManager": "pnpm@9.0.6", "packageManager": "pnpm@9.5.0",
"workspaces": [ "workspaces": [
"packages/frontend", "packages/frontend",
"packages/backend", "packages/backend",

View file

@ -4,7 +4,7 @@ import sharedConfig from '../shared/eslint.config.js';
export default [ export default [
...sharedConfig, ...sharedConfig,
{ {
ignores: ['**/node_modules', 'built', '@types/**/*'], ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'],
}, },
{ {
files: ['**/*.ts', '**/*.tsx'], files: ['**/*.ts', '**/*.tsx'],

View file

@ -23,7 +23,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
* *
*/ */
type Source = { type Source = {
url: string; url?: string;
port?: number; port?: number;
socket?: string; socket?: string;
chmodSocket?: string; chmodSocket?: string;
@ -31,9 +31,9 @@ type Source = {
db: { db: {
host: string; host: string;
port: number; port: number;
db: string; db?: string;
user: string; user?: string;
pass: string; pass?: string;
disableCache?: boolean; disableCache?: boolean;
extra?: { [x: string]: string }; extra?: { [x: string]: string };
}; };
@ -202,13 +202,17 @@ export function loadConfig(): Config {
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
const url = tryCreateUrl(config.url); const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
const version = meta.version; const version = meta.version;
const host = url.host; const host = url.host;
const hostname = url.hostname; const hostname = url.hostname;
const scheme = url.protocol.replace(/:$/, ''); const scheme = url.protocol.replace(/:$/, '');
const wsScheme = scheme.replace('http', 'ws'); const wsScheme = scheme.replace('http', 'ws');
const dbDb = config.db.db ?? process.env.DATABASE_DB ?? '';
const dbUser = config.db.user ?? process.env.DATABASE_USER ?? '';
const dbPass = config.db.pass ?? process.env.DATABASE_PASSWORD ?? '';
const externalMediaProxy = config.mediaProxy ? const externalMediaProxy = config.mediaProxy ?
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
: null; : null;
@ -231,7 +235,7 @@ export function loadConfig(): Config {
apiUrl: `${scheme}://${host}/api`, apiUrl: `${scheme}://${host}/api`,
authUrl: `${scheme}://${host}/auth`, authUrl: `${scheme}://${host}/auth`,
driveUrl: `${scheme}://${host}/files`, driveUrl: `${scheme}://${host}/files`,
db: config.db, db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass },
dbReplications: config.dbReplications, dbReplications: config.dbReplications,
dbSlaves: config.dbSlaves, dbSlaves: config.dbSlaves,
meilisearch: config.meilisearch, meilisearch: config.meilisearch,
@ -259,7 +263,7 @@ export function loadConfig(): Config {
deliverJobMaxAttempts: config.deliverJobMaxAttempts, deliverJobMaxAttempts: config.deliverJobMaxAttempts,
inboxJobMaxAttempts: config.inboxJobMaxAttempts, inboxJobMaxAttempts: config.inboxJobMaxAttempts,
proxyRemoteFiles: config.proxyRemoteFiles, proxyRemoteFiles: config.proxyRemoteFiles,
signToActivityPubGet: config.signToActivityPubGet, signToActivityPubGet: config.signToActivityPubGet ?? true,
mediaProxy: externalMediaProxy ?? internalMediaProxy, mediaProxy: externalMediaProxy ?? internalMediaProxy,
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy, externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
videoThumbnailGenerator: config.videoThumbnailGenerator ? videoThumbnailGenerator: config.videoThumbnailGenerator ?

View file

@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
// dummy
export const MAX_NOTE_TEXT_LENGTH = 3000; export const MAX_NOTE_TEXT_LENGTH = 3000;
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min

View file

@ -61,6 +61,7 @@ import { UserFollowingService } from './UserFollowingService.js';
import { UserKeypairService } from './UserKeypairService.js'; import { UserKeypairService } from './UserKeypairService.js';
import { UserListService } from './UserListService.js'; import { UserListService } from './UserListService.js';
import { UserMutingService } from './UserMutingService.js'; import { UserMutingService } from './UserMutingService.js';
import { UserRenoteMutingService } from './UserRenoteMutingService.js';
import { UserSuspendService } from './UserSuspendService.js'; import { UserSuspendService } from './UserSuspendService.js';
import { UserAuthService } from './UserAuthService.js'; import { UserAuthService } from './UserAuthService.js';
import { VideoProcessingService } from './VideoProcessingService.js'; import { VideoProcessingService } from './VideoProcessingService.js';
@ -203,6 +204,7 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService }; const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService }; const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService }; const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService', useExisting: UserRenoteMutingService };
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService }; const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService }; const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService }; const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
@ -350,6 +352,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserKeypairService, UserKeypairService,
UserListService, UserListService,
UserMutingService, UserMutingService,
UserRenoteMutingService,
UserSearchService, UserSearchService,
UserSuspendService, UserSuspendService,
UserAuthService, UserAuthService,
@ -493,6 +496,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserKeypairService, $UserKeypairService,
$UserListService, $UserListService,
$UserMutingService, $UserMutingService,
$UserRenoteMutingService,
$UserSearchService, $UserSearchService,
$UserSuspendService, $UserSuspendService,
$UserAuthService, $UserAuthService,
@ -637,6 +641,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserKeypairService, UserKeypairService,
UserListService, UserListService,
UserMutingService, UserMutingService,
UserRenoteMutingService,
UserSearchService, UserSearchService,
UserSuspendService, UserSuspendService,
UserAuthService, UserAuthService,
@ -779,6 +784,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserKeypairService, $UserKeypairService,
$UserListService, $UserListService,
$UserMutingService, $UserMutingService,
$UserRenoteMutingService,
$UserSearchService, $UserSearchService,
$UserSuspendService, $UserSuspendService,
$UserAuthService, $UserAuthService,

View file

@ -209,6 +209,10 @@ type SerializedAll<T> = {
[K in keyof T]: Serialized<T[K]>; [K in keyof T]: Serialized<T[K]>;
}; };
type UndefinedAsNullAll<T> = {
[K in keyof T]: T[K] extends undefined ? null : T[K];
}
export interface InternalEventTypes { export interface InternalEventTypes {
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; }; userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; }; userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
@ -247,43 +251,45 @@ export interface InternalEventTypes {
userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; }; userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
} }
type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>;
// name/messages(spec) pairs dictionary // name/messages(spec) pairs dictionary
export type GlobalEvents = { export type GlobalEvents = {
internal: { internal: {
name: 'internal'; name: 'internal';
payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>; payload: EventTypesToEventPayload<InternalEventTypes>;
}; };
broadcast: { broadcast: {
name: 'broadcast'; name: 'broadcast';
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>; payload: EventTypesToEventPayload<BroadcastTypes>;
}; };
main: { main: {
name: `mainStream:${MiUser['id']}`; name: `mainStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>; payload: EventTypesToEventPayload<MainEventTypes>;
}; };
drive: { drive: {
name: `driveStream:${MiUser['id']}`; name: `driveStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>; payload: EventTypesToEventPayload<DriveEventTypes>;
}; };
note: { note: {
name: `noteStream:${MiNote['id']}`; name: `noteStream:${MiNote['id']}`;
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>; payload: EventTypesToEventPayload<NoteStreamEventTypes>;
}; };
userList: { userList: {
name: `userListStream:${MiUserList['id']}`; name: `userListStream:${MiUserList['id']}`;
payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>; payload: EventTypesToEventPayload<UserListEventTypes>;
}; };
roleTimeline: { roleTimeline: {
name: `roleTimelineStream:${MiRole['id']}`; name: `roleTimelineStream:${MiRole['id']}`;
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>; payload: EventTypesToEventPayload<RoleTimelineEventTypes>;
}; };
antenna: { antenna: {
name: `antennaStream:${MiAntenna['id']}`; name: `antennaStream:${MiAntenna['id']}`;
payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>; payload: EventTypesToEventPayload<AntennaEventTypes>;
}; };
admin: { admin: {
name: `adminStream:${MiUser['id']}`; name: `adminStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>; payload: EventTypesToEventPayload<AdminEventTypes>;
}; };
notes: { notes: {
name: 'notesStream'; name: 'notesStream';
@ -291,11 +297,11 @@ export type GlobalEvents = {
}; };
reversi: { reversi: {
name: `reversiStream:${MiUser['id']}`; name: `reversiStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>; payload: EventTypesToEventPayload<ReversiEventTypes>;
}; };
reversiGame: { reversiGame: {
name: `reversiGameStream:${MiReversiGame['id']}`; name: `reversiGameStream:${MiReversiGame['id']}`;
payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>; payload: EventTypesToEventPayload<ReversiGameEventTypes>;
}; };
}; };

View file

@ -933,10 +933,13 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
} }
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL // 自分自身のHTL
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.userHost == null) {
if (note.fileIds.length > 0) { if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
} }
} }

View file

@ -505,14 +505,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
this.globalEventService.publishInternalEvent('userRoleAssigned', created); this.globalEventService.publishInternalEvent('userRoleAssigned', created);
if (role.isPublic) { const user = await this.usersRepository.findOneByOrFail({ id: userId });
if (role.isPublic && user.host === null) {
this.notificationService.createNotification(userId, 'roleAssigned', { this.notificationService.createNotification(userId, 'roleAssigned', {
roleId: roleId, roleId: roleId,
}); });
} }
if (moderator) { if (moderator) {
const user = await this.usersRepository.findOneByOrFail({ id: userId });
this.moderationLogService.log(moderator, 'assignRole', { this.moderationLogService.log(moderator, 'assignRole', {
roleId: roleId, roleId: roleId,
roleName: role.name, roleName: role.name,

View file

@ -279,8 +279,10 @@ export class UserFollowingService implements OnModuleInit {
}); });
// 通知を作成 // 通知を作成
this.notificationService.createNotification(follower.id, 'followRequestAccepted', { if (follower.host === null) {
}, followee.id); this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
}, followee.id);
}
} }
if (alreadyFollowed) return; if (alreadyFollowed) return;

View file

@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import type { RenoteMutingsRepository } from '@/models/_.js';
import type { MiRenoteMuting } from '@/models/RenoteMuting.js';
import { IdService } from '@/core/IdService.js';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
@Injectable()
export class UserRenoteMutingService {
constructor(
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
private idService: IdService,
private cacheService: CacheService,
) {
}
@bindThis
public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
await this.renoteMutingsRepository.insert({
id: this.idService.gen(),
muterId: user.id,
muteeId: target.id,
});
await this.cacheService.renoteMutingsCache.refresh(user.id);
}
@bindThis
public async unmute(mutings: MiRenoteMuting[]): Promise<void> {
if (mutings.length === 0) return;
await this.renoteMutingsRepository.delete({
id: In(mutings.map(m => m.id)),
});
const muterIds = [...new Set(mutings.map(m => m.muterId))];
for (const muterId of muterIds) {
await this.cacheService.renoteMutingsCache.refresh(muterId);
}
}
}

View file

@ -74,10 +74,10 @@ export class ApQuestionService {
//#region このサーバーに既に登録されているか //#region このサーバーに既に登録されているか
const note = await this.notesRepository.findOneBy({ uri }); const note = await this.notesRepository.findOneBy({ uri });
if (note == null) throw new Error('Question is not registed'); if (note == null) throw new Error('Question is not registered');
const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
if (poll == null) throw new Error('Question is not registed'); if (poll == null) throw new Error('Question is not registered');
//#endregion //#endregion
// resolve new Question object // resolve new Question object

View file

@ -4,6 +4,10 @@
*/ */
export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean { export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
if (!note) {
return false;
}
if (userIds.has(note.userId) && !ignoreAuthor) { if (userIds.has(note.userId) && !ignoreAuthor) {
return true; return true;
} }

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type JsonValue = JsonArray | JsonObject | string | number | boolean | null;
export type JsonObject = {[K in string]?: JsonValue};
export type JsonArray = JsonValue[];

View file

@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { PollVotesRepository, NotesRepository } from '@/models/_.js'; import type { PollVotesRepository, NotesRepository } from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { CacheService } from '@/core/CacheService.js';
import { NotificationService } from '@/core/NotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
@ -24,6 +25,7 @@ export class EndedPollNotificationProcessorService {
@Inject(DI.pollVotesRepository) @Inject(DI.pollVotesRepository)
private pollVotesRepository: PollVotesRepository, private pollVotesRepository: PollVotesRepository,
private cacheService: CacheService,
private notificationService: NotificationService, private notificationService: NotificationService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
) { ) {
@ -47,9 +49,12 @@ export class EndedPollNotificationProcessorService {
const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])]; const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])];
for (const userId of userIds) { for (const userId of userIds) {
this.notificationService.createNotification(userId, 'pollEnded', { const profile = await this.cacheService.userProfileCache.fetch(userId);
noteId: note.id, if (profile.userHost === null) {
}); this.notificationService.createNotification(userId, 'pollEnded', {
noteId: note.id,
});
}
} }
} }
} }

View file

@ -139,6 +139,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
timelineConfig = [ timelineConfig = [
`homeTimeline:${me.id}`, `homeTimeline:${me.id}`,
'localTimeline', 'localTimeline',
`localTimelineWithReplyTo:${me.id}`,
]; ];
} }

View file

@ -6,12 +6,11 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms'; import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { IdService } from '@/core/IdService.js';
import type { RenoteMutingsRepository } from '@/models/_.js';
import type { MiRenoteMuting } from '@/models/RenoteMuting.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js";
import type { RenoteMutingsRepository } from '@/models/_.js';
export const meta = { export const meta = {
tags: ['account'], tags: ['account'],
@ -62,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private renoteMutingsRepository: RenoteMutingsRepository, private renoteMutingsRepository: RenoteMutingsRepository,
private getterService: GetterService, private getterService: GetterService,
private idService: IdService, private userRenoteMutingService: UserRenoteMutingService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const muter = me; const muter = me;
@ -79,21 +78,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}); });
// Check if already muting // Check if already muting
const exist = await this.renoteMutingsRepository.findOneBy({ const exist = await this.renoteMutingsRepository.exists({
muterId: muter.id, where: {
muteeId: mutee.id, muterId: muter.id,
muteeId: mutee.id,
},
}); });
if (exist != null) { if (exist === true) {
throw new ApiError(meta.errors.alreadyMuting); throw new ApiError(meta.errors.alreadyMuting);
} }
// Create mute // Create mute
await this.renoteMutingsRepository.insert({ await this.userRenoteMutingService.mute(muter, mutee);
id: this.idService.gen(),
muterId: muter.id,
muteeId: mutee.id,
} as MiRenoteMuting);
}); });
} }
} }

View file

@ -5,10 +5,11 @@
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 { RenoteMutingsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js";
import type { RenoteMutingsRepository } from '@/models/_.js';
export const meta = { export const meta = {
tags: ['account'], tags: ['account'],
@ -53,6 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private renoteMutingsRepository: RenoteMutingsRepository, private renoteMutingsRepository: RenoteMutingsRepository,
private getterService: GetterService, private getterService: GetterService,
private userRenoteMutingService: UserRenoteMutingService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const muter = me; const muter = me;
@ -79,9 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
// Delete mute // Delete mute
await this.renoteMutingsRepository.delete({ await this.userRenoteMutingService.unmute([exist]);
id: exist.id,
});
}); });
} }
} }

View file

@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -74,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService, private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set<string>();
const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users
if (!iAmModerator) { if (!iAmModerator) {
const user = await this.cacheService.findUserById(ps.userId); const user = await this.cacheService.findUserById(ps.userId);
@ -85,8 +87,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if ((me == null || me.id !== ps.userId) && !profile.publicReactions) { if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
throw new ApiError(meta.errors.reactionsNotPublic); throw new ApiError(meta.errors.reactionsNotPublic);
} }
// early return if me is blocked by requesting user
if (userIdsWhoBlockingMe.has(ps.userId)) {
return [];
}
} }
const userIdsWhoMeMuting = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Set<string>();
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('reaction.userId = :userId', { userId: ps.userId }) .andWhere('reaction.userId = :userId', { userId: ps.userId })
@ -94,9 +103,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me); this.queryService.generateVisibilityQuery(query, me);
const reactions = await query const reactions = (await query
.limit(ps.limit) .limit(ps.limit)
.getMany(); .getMany()).filter(reaction => {
if (reaction.note?.userId === ps.userId) return true; // we can see reactions to note of requesting user
if (me && isUserRelated(reaction.note, userIdsWhoBlockingMe)) return false;
if (me && isUserRelated(reaction.note, userIdsWhoMeMuting)) return false;
return true;
});
return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true }); return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true });
}); });

View file

@ -14,6 +14,7 @@ import { CacheService } from '@/core/CacheService.js';
import { MiFollowing, MiUserProfile } from '@/models/_.js'; import { MiFollowing, MiUserProfile } from '@/models/_.js';
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js'; import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import type { JsonObject } from '@/misc/json-value.js';
import type { ChannelsService } from './ChannelsService.js'; import type { ChannelsService } from './ChannelsService.js';
import type { EventEmitter } from 'events'; import type { EventEmitter } from 'events';
import type Channel from './channel.js'; import type Channel from './channel.js';
@ -28,7 +29,7 @@ export default class Connection {
private wsConnection: WebSocket.WebSocket; private wsConnection: WebSocket.WebSocket;
public subscriber: StreamEventEmitter; public subscriber: StreamEventEmitter;
private channels: Channel[] = []; private channels: Channel[] = [];
private subscribingNotes: any = {}; private subscribingNotes: Partial<Record<string, number>> = {};
private cachedNotes: Packed<'Note'>[] = []; private cachedNotes: Packed<'Note'>[] = [];
public userProfile: MiUserProfile | null = null; public userProfile: MiUserProfile | null = null;
public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {}; public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
@ -101,7 +102,7 @@ export default class Connection {
*/ */
@bindThis @bindThis
private async onWsConnectionMessage(data: WebSocket.RawData) { private async onWsConnectionMessage(data: WebSocket.RawData) {
let obj: Record<string, any>; let obj: JsonObject;
try { try {
obj = JSON.parse(data.toString()); obj = JSON.parse(data.toString());
@ -111,6 +112,8 @@ export default class Connection {
const { type, body } = obj; const { type, body } = obj;
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
switch (type) { switch (type) {
case 'readNotification': this.onReadNotification(body); break; case 'readNotification': this.onReadNotification(body); break;
case 'subNote': this.onSubscribeNote(body); break; case 'subNote': this.onSubscribeNote(body); break;
@ -151,7 +154,7 @@ export default class Connection {
} }
@bindThis @bindThis
private readNote(body: any) { private readNote(body: JsonObject) {
const id = body.id; const id = body.id;
const note = this.cachedNotes.find(n => n.id === id); const note = this.cachedNotes.find(n => n.id === id);
@ -163,7 +166,7 @@ export default class Connection {
} }
@bindThis @bindThis
private onReadNotification(payload: any) { private onReadNotification(payload: JsonObject) {
this.notificationService.readAllNotification(this.user!.id); this.notificationService.readAllNotification(this.user!.id);
} }
@ -171,16 +174,14 @@ export default class Connection {
* 稿 * 稿
*/ */
@bindThis @bindThis
private onSubscribeNote(payload: any) { private onSubscribeNote(payload: JsonObject) {
if (!payload.id) return; if (!payload.id || typeof payload.id !== 'string') return;
if (this.subscribingNotes[payload.id] == null) { const current = this.subscribingNotes[payload.id] ?? 0;
this.subscribingNotes[payload.id] = 0; const updated = current + 1;
} this.subscribingNotes[payload.id] = updated;
this.subscribingNotes[payload.id]++; if (updated === 1) {
if (this.subscribingNotes[payload.id] === 1) {
this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
} }
} }
@ -189,11 +190,14 @@ export default class Connection {
* 稿 * 稿
*/ */
@bindThis @bindThis
private onUnsubscribeNote(payload: any) { private onUnsubscribeNote(payload: JsonObject) {
if (!payload.id) return; if (!payload.id || typeof payload.id !== 'string') return;
this.subscribingNotes[payload.id]--; const current = this.subscribingNotes[payload.id];
if (this.subscribingNotes[payload.id] <= 0) { if (current == null) return;
const updated = current - 1;
this.subscribingNotes[payload.id] = updated;
if (updated <= 0) {
delete this.subscribingNotes[payload.id]; delete this.subscribingNotes[payload.id];
this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage); this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage);
} }
@ -212,17 +216,22 @@ export default class Connection {
* *
*/ */
@bindThis @bindThis
private onChannelConnectRequested(payload: any) { private onChannelConnectRequested(payload: JsonObject) {
const { channel, id, params, pong } = payload; const { channel, id, params, pong } = payload;
this.connectChannel(id, params, channel, pong); if (typeof id !== 'string') return;
if (typeof channel !== 'string') return;
if (typeof pong !== 'boolean' && typeof pong !== 'undefined' && pong !== null) return;
if (typeof params !== 'undefined' && (typeof params !== 'object' || params === null || Array.isArray(params))) return;
this.connectChannel(id, params, channel, pong ?? undefined);
} }
/** /**
* *
*/ */
@bindThis @bindThis
private onChannelDisconnectRequested(payload: any) { private onChannelDisconnectRequested(payload: JsonObject) {
const { id } = payload; const { id } = payload;
if (typeof id !== 'string') return;
this.disconnectChannel(id); this.disconnectChannel(id);
} }
@ -230,7 +239,7 @@ export default class Connection {
* *
*/ */
@bindThis @bindThis
public sendMessageToWs(type: string, payload: any) { public sendMessageToWs(type: string, payload: JsonObject) {
this.wsConnection.send(JSON.stringify({ this.wsConnection.send(JSON.stringify({
type: type, type: type,
body: payload, body: payload,
@ -241,7 +250,7 @@ export default class Connection {
* *
*/ */
@bindThis @bindThis
public connectChannel(id: string, params: any, channel: string, pong = false) { public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
const channelService = this.channelsService.getChannelService(channel); const channelService = this.channelsService.getChannelService(channel);
if (channelService.requireCredential && this.user == null) { if (channelService.requireCredential && this.user == null) {
@ -288,7 +297,11 @@ export default class Connection {
* @param data * @param data
*/ */
@bindThis @bindThis
private onChannelMessageRequested(data: any) { private onChannelMessageRequested(data: JsonObject) {
if (typeof data.id !== 'string') return;
if (typeof data.type !== 'string') return;
if (typeof data.body === 'undefined') return;
const channel = this.channels.find(c => c.id === data.id); const channel = this.channels.find(c => c.id === data.id);
if (channel != null && channel.onMessage != null) { if (channel != null && channel.onMessage != null) {
channel.onMessage(data.type, data.body); channel.onMessage(data.type, data.body);

View file

@ -8,6 +8,7 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import type Connection from './Connection.js'; import type Connection from './Connection.js';
/** /**
@ -81,10 +82,12 @@ export default abstract class Channel {
this.connection = connection; this.connection = connection;
} }
public send(payload: { type: string, body: JsonValue }): void
public send(type: string, payload: JsonValue): void
@bindThis @bindThis
public send(typeOrPayload: any, payload?: any) { public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) {
const type = payload === undefined ? typeOrPayload.type : typeOrPayload; const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string);
const body = payload === undefined ? typeOrPayload.body : payload; const body = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).body : payload;
this.connection.sendMessageToWs('channel', { this.connection.sendMessageToWs('channel', {
id: this.id, id: this.id,
@ -93,11 +96,11 @@ export default abstract class Channel {
}); });
} }
public abstract init(params: any): void; public abstract init(params: JsonObject): void;
public dispose?(): void; public dispose?(): void;
public onMessage?(type: string, body: any): void; public onMessage?(type: string, body: JsonValue): void;
} }
export type MiChannelService<T extends boolean> = { export type MiChannelService<T extends boolean> = {

View file

@ -5,6 +5,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class AdminChannel extends Channel { class AdminChannel extends Channel {
@ -14,7 +15,7 @@ class AdminChannel extends Channel {
public static kind = 'read:admin:stream'; public static kind = 'read:admin:stream';
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
// Subscribe admin stream // Subscribe admin stream
this.subscriber.on(`adminStream:${this.user!.id}`, data => { this.subscriber.on(`adminStream:${this.user!.id}`, data => {
this.send(data); this.send(data);

View file

@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.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 type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class AntennaChannel extends Channel { class AntennaChannel extends Channel {
@ -27,8 +28,9 @@ class AntennaChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.antennaId = params.antennaId as string; if (typeof params.antennaId !== 'string') return;
this.antennaId = params.antennaId;
// Subscribe stream // Subscribe stream
this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent); this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);

View file

@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class ChannelChannel extends Channel { class ChannelChannel extends Channel {
@ -27,8 +28,9 @@ class ChannelChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.channelId = params.channelId as string; if (typeof params.channelId !== 'string') return;
this.channelId = params.channelId;
// Subscribe stream // Subscribe stream
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);

View file

@ -5,6 +5,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class DriveChannel extends Channel { class DriveChannel extends Channel {
@ -14,7 +15,7 @@ class DriveChannel extends Channel {
public static kind = 'read:account'; public static kind = 'read:account';
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
// Subscribe drive stream // Subscribe drive stream
this.subscriber.on(`driveStream:${this.user!.id}`, data => { this.subscriber.on(`driveStream:${this.user!.id}`, data => {
this.send(data); this.send(data);

View file

@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class GlobalTimelineChannel extends Channel { class GlobalTimelineChannel extends Channel {
@ -32,12 +33,12 @@ class GlobalTimelineChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.gtlAvailable) return; if (!policies.gtlAvailable) return;
this.withRenotes = params.withRenotes ?? true; this.withRenotes = !!(params.withRenotes ?? true);
this.withFiles = params.withFiles ?? false; this.withFiles = !!(params.withFiles ?? false);
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);

View file

@ -9,6 +9,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class HashtagChannel extends Channel { class HashtagChannel extends Channel {
@ -28,11 +29,11 @@ class HashtagChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
if (!Array.isArray(params.q)) return;
if (!params.q.every(x => Array.isArray(x) && x.every(y => typeof y === 'string'))) return;
this.q = params.q; this.q = params.q;
if (this.q == null) return;
// Subscribe stream // Subscribe stream
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
} }

View file

@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class HomeTimelineChannel extends Channel { class HomeTimelineChannel extends Channel {
@ -29,9 +30,9 @@ class HomeTimelineChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.withRenotes = params.withRenotes ?? true; this.withRenotes = !!(params.withRenotes ?? true);
this.withFiles = params.withFiles ?? false; this.withFiles = !!(params.withFiles ?? false);
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
} }

View file

@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class HybridTimelineChannel extends Channel { class HybridTimelineChannel extends Channel {
@ -34,13 +35,13 @@ class HybridTimelineChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any): Promise<void> { public async init(params: JsonObject): Promise<void> {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return; if (!policies.ltlAvailable) return;
this.withRenotes = params.withRenotes ?? true; this.withRenotes = !!(params.withRenotes ?? true);
this.withReplies = params.withReplies ?? false; this.withReplies = !!(params.withReplies ?? false);
this.withFiles = params.withFiles ?? false; this.withFiles = !!(params.withFiles ?? false);
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);

View file

@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js'; import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class LocalTimelineChannel extends Channel { class LocalTimelineChannel extends Channel {
@ -33,13 +34,13 @@ class LocalTimelineChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return; if (!policies.ltlAvailable) return;
this.withRenotes = params.withRenotes ?? true; this.withRenotes = !!(params.withRenotes ?? true);
this.withReplies = params.withReplies ?? false; this.withReplies = !!(params.withReplies ?? false);
this.withFiles = params.withFiles ?? false; this.withFiles = !!(params.withFiles ?? false);
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);

View file

@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js'; import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class MainChannel extends Channel { class MainChannel extends Channel {
@ -25,7 +26,7 @@ class MainChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
// Subscribe main stream channel // Subscribe main stream channel
this.subscriber.on(`mainStream:${this.user!.id}`, async data => { this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
switch (data.type) { switch (data.type) {

View file

@ -6,6 +6,7 @@
import Xev from 'xev'; import Xev from 'xev';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
const ev = new Xev(); const ev = new Xev();
@ -22,19 +23,22 @@ class QueueStatsChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
ev.addListener('queueStats', this.onStats); ev.addListener('queueStats', this.onStats);
} }
@bindThis @bindThis
private onStats(stats: any) { private onStats(stats: JsonObject) {
this.send('stats', stats); this.send('stats', stats);
} }
@bindThis @bindThis
public onMessage(type: string, body: any) { public onMessage(type: string, body: JsonValue) {
switch (type) { switch (type) {
case 'requestLog': case 'requestLog':
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
if (typeof body.id !== 'string') return;
if (typeof body.length !== 'number') return;
ev.once(`queueStatsLog:${body.id}`, statsLog => { ev.once(`queueStatsLog:${body.id}`, statsLog => {
this.send('statsLog', statsLog); this.send('statsLog', statsLog);
}); });

View file

@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { ReversiService } from '@/core/ReversiService.js'; import { ReversiService } from '@/core/ReversiService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class ReversiGameChannel extends Channel { class ReversiGameChannel extends Channel {
@ -28,25 +29,41 @@ class ReversiGameChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.gameId = params.gameId as string; if (typeof params.gameId !== 'string') return;
this.gameId = params.gameId;
this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send); this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send);
} }
@bindThis @bindThis
public onMessage(type: string, body: any) { public onMessage(type: string, body: JsonValue) {
switch (type) { switch (type) {
case 'ready': this.ready(body); break; case 'ready':
case 'updateSettings': this.updateSettings(body.key, body.value); break; if (typeof body !== 'boolean') return;
case 'cancel': this.cancelGame(); break; this.ready(body);
case 'putStone': this.putStone(body.pos, body.id); break; break;
case 'updateSettings':
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
if (typeof body.key !== 'string') return;
if (typeof body.value !== 'object' || body.value === null || Array.isArray(body.value)) return;
this.updateSettings(body.key, body.value);
break;
case 'cancel':
this.cancelGame();
break;
case 'putStone':
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
if (typeof body.pos !== 'number') return;
if (typeof body.id !== 'string') return;
this.putStone(body.pos, body.id);
break;
case 'claimTimeIsUp': this.claimTimeIsUp(); break; case 'claimTimeIsUp': this.claimTimeIsUp(); break;
} }
} }
@bindThis @bindThis
private async updateSettings(key: string, value: any) { private async updateSettings(key: string, value: JsonObject) {
if (this.user == null) return; if (this.user == null) return;
this.reversiService.updateSettings(this.gameId!, this.user, key, value); this.reversiService.updateSettings(this.gameId!, this.user, key, value);

View file

@ -5,6 +5,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class ReversiChannel extends Channel { class ReversiChannel extends Channel {
@ -21,7 +22,7 @@ class ReversiChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.subscriber.on(`reversiStream:${this.user!.id}`, this.send); this.subscriber.on(`reversiStream:${this.user!.id}`, this.send);
} }

View file

@ -8,6 +8,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class RoleTimelineChannel extends Channel { class RoleTimelineChannel extends Channel {
@ -28,8 +29,9 @@ class RoleTimelineChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.roleId = params.roleId as string; if (typeof params.roleId !== 'string') return;
this.roleId = params.roleId;
this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent); this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent);
} }

View file

@ -6,6 +6,7 @@
import Xev from 'xev'; import Xev from 'xev';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
const ev = new Xev(); const ev = new Xev();
@ -22,19 +23,20 @@ class ServerStatsChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
ev.addListener('serverStats', this.onStats); ev.addListener('serverStats', this.onStats);
} }
@bindThis @bindThis
private onStats(stats: any) { private onStats(stats: JsonObject) {
this.send('stats', stats); this.send('stats', stats);
} }
@bindThis @bindThis
public onMessage(type: string, body: any) { public onMessage(type: string, body: JsonValue) {
switch (type) { switch (type) {
case 'requestLog': case 'requestLog':
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
ev.once(`serverStatsLog:${body.id}`, statsLog => { ev.once(`serverStatsLog:${body.id}`, statsLog => {
this.send('statsLog', statsLog); this.send('statsLog', statsLog);
}); });

View file

@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class UserListChannel extends Channel { class UserListChannel extends Channel {
@ -36,10 +37,11 @@ class UserListChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.listId = params.listId as string; if (typeof params.listId !== 'string') return;
this.withFiles = params.withFiles ?? false; this.listId = params.listId;
this.withRenotes = params.withRenotes ?? true; this.withFiles = !!(params.withFiles ?? false);
this.withRenotes = !!(params.withRenotes ?? true);
// Check existence and owner // Check existence and owner
const listExist = await this.userListsRepository.exists({ const listExist = await this.userListsRepository.exists({

View file

@ -9,8 +9,8 @@
import * as assert from 'assert'; import * as assert from 'assert';
import { setTimeout } from 'node:timers/promises'; import { setTimeout } from 'node:timers/promises';
import { Redis } from 'ioredis'; import { Redis } from 'ioredis';
import { loadConfig } from '@/config.js';
import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js'; import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js';
import { loadConfig } from '@/config.js';
function genHost() { function genHost() {
return randomString() + '.example.com'; return randomString() + '.example.com';
@ -492,6 +492,44 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('FTT: ローカルユーザーの HTL にはプッシュされる', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', {
userId: alice.id,
}, bob);
const aliceNote = await post(alice, { text: 'I\'m Alice.' });
const bobNote = await post(bob, { text: 'I\'m Bob.' });
const carolNote = await post(carol, { text: 'I\'m Carol.' });
await waitForPushToTl();
// NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる
assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 1);
const bobHTL = await redisForTimelines.lrange(`list:homeTimeline:${bob.id}`, 0, -1);
assert.strictEqual(bobHTL.includes(aliceNote.id), true);
assert.strictEqual(bobHTL.includes(bobNote.id), true);
assert.strictEqual(bobHTL.includes(carolNote.id), false);
});
test.concurrent('FTT: リモートユーザーの HTL にはプッシュされない', async () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
await api('following/create', {
userId: alice.id,
}, bob);
await post(alice, { text: 'I\'m Alice.' });
await post(bob, { text: 'I\'m Bob.' });
await waitForPushToTl();
// NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる
assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0);
});
}); });
describe('Local TL', () => { describe('Local TL', () => {

View file

@ -53,7 +53,6 @@ await fs.readFile(
'../../assets/**', '../../assets/**',
'../../fluent-emojis/**', '../../fluent-emojis/**',
'../../locales/ja-JP.yml', '../../locales/ja-JP.yml',
'../../misskey-assets/**',
'assets/**', 'assets/**',
'public/**', 'public/**',
'../../pnpm-lock.yaml', '../../pnpm-lock.yaml',

View file

@ -24,7 +24,7 @@
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "5.0.7", "@rollup/plugin-replace": "5.0.7",
"@rollup/pluginutils": "5.1.0", "@rollup/pluginutils": "5.1.0",
"@syuilo/aiscript": "0.18.0", "@syuilo/aiscript": "0.19.0",
"@tabler/icons-webfont": "3.3.0", "@tabler/icons-webfont": "3.3.0",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.0.5", "@vitejs/plugin-vue": "5.0.5",

View file

@ -5,6 +5,7 @@
import { createApp, defineAsyncComponent, markRaw } from 'vue'; import { createApp, defineAsyncComponent, markRaw } from 'vue';
import { common } from './common.js'; import { common } from './common.js';
import type * as Misskey from 'misskey-js';
import { ui } from '@/config.js'; import { ui } from '@/config.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { alert, confirm, popup, post, toast } from '@/os.js'; import { alert, confirm, popup, post, toast } from '@/os.js';
@ -113,7 +114,7 @@ export async function mainBoot() {
}); });
} }
stream.on('announcementCreated', (ev) => { function onAnnouncementCreated (ev: { announcement: Misskey.entities.Announcement }) {
const announcement = ev.announcement; const announcement = ev.announcement;
if (announcement.display === 'dialog') { if (announcement.display === 'dialog') {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
@ -122,7 +123,9 @@ export async function mainBoot() {
closed: () => dispose(), closed: () => dispose(),
}); });
} }
}); }
stream.on('announcementCreated', onAnnouncementCreated);
if ($i.isDeleted) { if ($i.isDeleted) {
alert({ alert({
@ -315,6 +318,9 @@ export async function mainBoot() {
updateAccount({ hasUnreadAnnouncement: false }); updateAccount({ hasUnreadAnnouncement: false });
}); });
// 個人宛てお知らせが発行されたとき
main.on('announcementCreated', onAnnouncementCreated);
// トークンが再生成されたとき // トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる // このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => { main.on('myTokenRegenerated', () => {

View file

@ -5,9 +5,12 @@
import { createApp, defineAsyncComponent } from 'vue'; import { createApp, defineAsyncComponent } from 'vue';
import { common } from './common.js'; import { common } from './common.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
export async function subBoot() { export async function subBoot() {
const { isClientUpdated } = await common(() => createApp( const { isClientUpdated } = await common(() => createApp(
defineAsyncComponent(() => import('@/ui/minimum.vue')), defineAsyncComponent(() => import('@/ui/minimum.vue')),
)); ));
emojiPicker.init();
} }

View file

@ -30,7 +30,7 @@ import * as os from '@/os.js';
import MkLoading from '@/components/global/MkLoading.vue'; import MkLoading from '@/components/global/MkLoading.vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{ const props = defineProps<{
code: string; code: string;

View file

@ -39,7 +39,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{

View file

@ -151,22 +151,26 @@ function drawImage(bitmap: CanvasImageSource) {
} }
function drawAvg() { function drawAvg() {
if (!canvas.value || !props.hash) return; if (!canvas.value) return;
const color = (props.hash != null && extractAvgColorFromBlurhash(props.hash)) || '#888';
const ctx = canvas.value.getContext('2d'); const ctx = canvas.value.getContext('2d');
if (!ctx) return; if (!ctx) return;
// avgColor // avgColor
ctx.beginPath(); ctx.beginPath();
ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888'; ctx.fillStyle = color;
ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value); ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
} }
async function draw() { async function draw() {
if (props.hash == null) return; if (import.meta.env.MODE === 'test' && props.hash == null) return;
drawAvg(); drawAvg();
if (props.hash == null) return;
if (props.onlyAvgColor) return; if (props.onlyAvgColor) return;
const work = await canvasPromise; const work = await canvasPromise;

View file

@ -62,7 +62,7 @@ import { computed } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';

View file

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View file

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@contextmenu.stop @contextmenu.stop
@keydown.stop @keydown.stop
> >
<button v-if="hide" :class="$style.hidden" @click="hide = false"> <button v-if="hide" :class="$style.hidden" @click="show">
<div :class="$style.hiddenTextWrapper"> <div :class="$style.hiddenTextWrapper">
<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b> <b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
<b v-else style="display: block;"><i class="ti ti-music"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b> <b v-else style="display: block;"><i class="ti ti-music"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
@ -156,6 +156,18 @@ const audioEl = shallowRef<HTMLAudioElement>();
// eslint-disable-next-line vue/no-setup-props-reactivity-loss // eslint-disable-next-line vue/no-setup-props-reactivity-loss
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore')); const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore'));
async function show() {
if (props.audio.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts.sensitiveMediaRevealConfirm,
});
if (canceled) return;
}
hide.value = false;
}
// Menu // Menu
const menuShowing = ref(false); const menuShowing = ref(false);

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div :class="$style.root"> <div :class="$style.root">
<MkMediaAudio v-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/> <MkMediaAudio v-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/>
<div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="hide = false"> <div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="show">
<span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span> <span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
<b>{{ i18n.ts.sensitive }}</b> <b>{{ i18n.ts.sensitive }}</b>
<span>{{ i18n.ts.clickToShow }}</span> <span>{{ i18n.ts.clickToShow }}</span>
@ -24,24 +24,30 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { shallowRef, watch, ref } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
import MkMediaAudio from '@/components/MkMediaAudio.vue'; import MkMediaAudio from '@/components/MkMediaAudio.vue';
const props = withDefaults(defineProps<{ const props = defineProps<{
media: Misskey.entities.DriveFile; media: Misskey.entities.DriveFile;
}>(), { }>();
});
const audioEl = shallowRef<HTMLAudioElement>();
const hide = ref(true); const hide = ref(true);
watch(audioEl, () => { async function show() {
if (audioEl.value) { if (props.media.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
audioEl.value.volume = 0.3; const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts.sensitiveMediaRevealConfirm,
});
if (canceled) return;
} }
});
hide.value = false;
}
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -83,11 +83,21 @@ const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
: props.image.thumbnailUrl, : props.image.thumbnailUrl,
); );
function onclick() { async function onclick(ev: MouseEvent) {
if (!props.controls) { if (!props.controls) {
return; return;
} }
if (hide.value) { if (hide.value) {
ev.stopPropagation();
if (props.image.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts.sensitiveMediaRevealConfirm,
});
if (canceled) return;
}
hide.value = false; hide.value = false;
} }
} }

View file

@ -138,15 +138,13 @@ onMounted(() => {
pswpModule: PhotoSwipe, pswpModule: PhotoSwipe,
}); });
lightbox.on('itemData', (ev) => { lightbox.addFilter('itemData', (itemData) => {
const { itemData } = ev;
// element is children // element is children
const { element } = itemData; const { element } = itemData;
const id = element?.dataset.id; const id = element?.dataset.id;
const file = props.mediaList.find(media => media.id === id); const file = props.mediaList.find(media => media.id === id);
if (!file) return; if (!file) return itemData;
itemData.src = file.url; itemData.src = file.url;
itemData.w = Number(file.properties.width); itemData.w = Number(file.properties.width);
@ -158,6 +156,8 @@ onMounted(() => {
itemData.alt = file.comment ?? file.name; itemData.alt = file.comment ?? file.name;
itemData.comment = file.comment ?? file.name; itemData.comment = file.comment ?? file.name;
itemData.thumbCropped = true; itemData.thumbCropped = true;
return itemData;
}); });
lightbox.on('uiRegister', () => { lightbox.on('uiRegister', () => {

View file

@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@contextmenu.stop @contextmenu.stop
@keydown.stop @keydown.stop
> >
<button v-if="hide" :class="$style.hidden" @click="hide = false"> <button v-if="hide" :class="$style.hidden" @click="show">
<div :class="$style.hiddenTextWrapper"> <div :class="$style.hiddenTextWrapper">
<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> <b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b> <b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
@ -176,6 +176,18 @@ function hasFocus() {
// eslint-disable-next-line vue/no-setup-props-reactivity-loss // eslint-disable-next-line vue/no-setup-props-reactivity-loss
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore')); const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
async function show() {
if (props.video.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts.sensitiveMediaRevealConfirm,
});
if (canceled) return;
}
hide.value = false;
}
// Menu // Menu
const menuShowing = ref(false); const menuShowing = ref(false);

View file

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
}" }"
:style="{ :style="{
width: (width && !asDrawer) ? `${width}px` : '', width: (width && !asDrawer) ? `${width}px` : '',
maxHeight: maxHeight ? `${maxHeight}px` : '', maxHeight: maxHeight ? `min(${maxHeight}px, calc(100dvh - 32px))` : 'calc(100dvh - 32px)',
}" }"
@keydown.stop="() => {}" @keydown.stop="() => {}"
@contextmenu.self.prevent="() => {}" @contextmenu.self.prevent="() => {}"

View file

@ -33,7 +33,7 @@ import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue'
import RouterView from '@/components/global/RouterView.vue'; import RouterView from '@/components/global/RouterView.vue';
import MkWindow from '@/components/MkWindow.vue'; import MkWindow from '@/components/MkWindow.vue';
import { popout as _popout } from '@/scripts/popout.js'; import { popout as _popout } from '@/scripts/popout.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js'; import { url } from '@/config.js';
import { useScrollPositionManager } from '@/nirax.js'; import { useScrollPositionManager } from '@/nirax.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View file

@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput> </MkInput>
</section> </section>
<section v-else-if="expiration === 'after'"> <section v-else-if="expiration === 'after'">
<MkInput v-model="after" small type="number" class="input"> <MkInput v-model="after" small type="number" min="1" class="input">
<template #label>{{ i18n.ts._poll.duration }}</template> <template #label>{{ i18n.ts._poll.duration }}</template>
</MkInput> </MkInput>
<MkSelect v-model="unit" small> <MkSelect v-model="unit" small>

View file

@ -81,6 +81,7 @@ function getReactionName(reaction: string): string {
} }
.user { .user {
display: flex;
line-height: 24px; line-height: 24px;
padding-top: 4px; padding-top: 4px;
white-space: nowrap; white-space: nowrap;

View file

@ -87,7 +87,7 @@ const host = ref(toUnicode(configHost));
const totpLogin = ref(false); const totpLogin = ref(false);
const isBackupCode = ref(false); const isBackupCode = ref(false);
const queryingKey = ref(false); const queryingKey = ref(false);
const credentialRequest = ref<CredentialRequestOptions | null>(null); let credentialRequest: CredentialRequestOptions | null = null;
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'login', v: any): void; (ev: 'login', v: any): void;
@ -122,14 +122,14 @@ function onLogin(res: any): Promise<void> | void {
} }
async function queryKey(): Promise<void> { async function queryKey(): Promise<void> {
if (credentialRequest.value == null) return; if (credentialRequest == null) return;
queryingKey.value = true; queryingKey.value = true;
await webAuthnRequest(credentialRequest.value) await webAuthnRequest(credentialRequest)
.catch(() => { .catch(() => {
queryingKey.value = false; queryingKey.value = false;
return Promise.reject(null); return Promise.reject(null);
}).then(credential => { }).then(credential => {
credentialRequest.value = null; credentialRequest = null;
queryingKey.value = false; queryingKey.value = false;
signing.value = true; signing.value = true;
return misskeyApi('signin', { return misskeyApi('signin', {
@ -160,7 +160,7 @@ function onSubmit(): void {
}).then(res => { }).then(res => {
totpLogin.value = true; totpLogin.value = true;
signing.value = false; signing.value = false;
credentialRequest.value = parseRequestOptionsFromJSON({ credentialRequest = parseRequestOptionsFromJSON({
publicKey: res, publicKey: res,
}); });
}) })

View file

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
scrolling="no" scrolling="no"
:allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')" :allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')"
:class="$style.playerIframe" :class="$style.playerIframe"
:src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :src="transformPlayerUrl(player.url)"
:style="{ border: 0 }" :style="{ border: 0 }"
></iframe> ></iframe>
<span v-else>invalid url</span> <span v-else>invalid url</span>
@ -91,6 +91,7 @@ import * as os from '@/os.js';
import { deviceKind } from '@/scripts/device-kind.js'; import { deviceKind } from '@/scripts/device-kind.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { versatileLang } from '@/scripts/intl-const.js'; import { versatileLang } from '@/scripts/intl-const.js';
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
type SummalyResult = Awaited<ReturnType<typeof summaly>>; type SummalyResult = Awaited<ReturnType<typeof summaly>>;

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div class="_panel" :class="$style.root"> <div class="_panel" :class="$style.root">
<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div> <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div>
<MkAvatar :class="$style.avatar" :user="user" indicator/> <MkAvatar :class="$style.avatar" :user="user" indicator/>
<div :class="$style.title"> <div :class="$style.title">
<MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> <MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
@ -41,6 +41,8 @@ import { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import { defaultStore } from '@/store.js';
defineProps<{ defineProps<{
user: Misskey.entities.UserDetailed; user: Misskey.entities.UserDetailed;

View file

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
> >
<div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }"> <div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
<div v-if="user != null"> <div v-if="user != null">
<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"> <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''">
<span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span> <span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
</div> </div>
<svg viewBox="0 0 128 128" :class="$style.avatarBack"> <svg viewBox="0 0 128 128" :class="$style.avatarBack">
@ -67,6 +67,7 @@ import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
const props = defineProps<{ const props = defineProps<{
showing: boolean; showing: boolean;

View file

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="poamfof"> <div class="poamfof">
<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in"> <Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="player.url && (player.url.startsWith('http://') || player.url.startsWith('https://'))" class="player"> <div v-if="player.url && (player.url.startsWith('http://') || player.url.startsWith('https://'))" class="player">
<iframe v-if="!fetching" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/> <iframe v-if="!fetching" :src="transformPlayerUrl(player.url)" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
</div> </div>
<span v-else>invalid url</span> <span v-else>invalid url</span>
</Transition> </Transition>
@ -27,6 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue'; import { ref } from 'vue';
import MkWindow from '@/components/MkWindow.vue'; import MkWindow from '@/components/MkWindow.vue';
import { versatileLang } from '@/scripts/intl-const.js'; import { versatileLang } from '@/scripts/intl-const.js';
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
const props = defineProps<{ const props = defineProps<{

View file

@ -16,7 +16,7 @@ export type MkABehavior = 'window' | 'browser' | null;
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, shallowRef } from 'vue'; import { computed, inject, shallowRef } from 'vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js'; import { url } from '@/config.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';

View file

@ -31,7 +31,7 @@ import { defaultStore } from '@/store.js';
import { customEmojisMap } from '@/custom-emojis.js'; import { customEmojisMap } from '@/custom-emojis.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { misskeyApiGet } from '@/scripts/misskey-api.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue'; import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';

View file

@ -14,7 +14,7 @@ import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@/scripts/emoji-
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js'; import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View file

@ -65,7 +65,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
const validTime = (t: string | boolean | null | undefined) => { const validTime = (t: string | boolean | null | undefined) => {
if (t == null) return null; if (t == null) return null;
if (typeof t === 'boolean') return null; if (typeof t === 'boolean') return null;
return t.match(/^[0-9.]+s$/) ? t : null; return t.match(/^\-?[0-9.]+s$/) ? t : null;
}; };
const validColor = (c: unknown): string | null => { const validColor = (c: unknown): string | null => {

View file

@ -53,7 +53,7 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
const current = resolveNested(router.current)!; const current = resolveNested(router.current)!;
const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage); const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
const currentPageProps = ref(current.props); const currentPageProps = ref(current.props);
const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props))); const key = ref(router.getCurrentKey() + JSON.stringify(Object.fromEntries(current.props)));
function onChange({ resolved, key: newKey }) { function onChange({ resolved, key: newKey }) {
const current = resolveNested(resolved); const current = resolveNested(resolved);

View file

@ -22,7 +22,7 @@ import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
import MkPopupMenu from '@/components/MkPopupMenu.vue'; import MkPopupMenu from '@/components/MkPopupMenu.vue';
import MkContextMenu from '@/components/MkContextMenu.vue'; import MkContextMenu from '@/components/MkContextMenu.vue';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { pleaseLogin } from '@/scripts/please-login.js'; import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js'; import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';

View file

@ -243,6 +243,21 @@ const patronsWithIcon = [{
}, { }, {
name: '越貝鯛丸', name: '越貝鯛丸',
icon: 'https://assets.misskey-hub.net/patrons/86c7374de37849b882d8ebbc833dc968.jpg', icon: 'https://assets.misskey-hub.net/patrons/86c7374de37849b882d8ebbc833dc968.jpg',
}, {
name: '☔あめ🍬(灬˘╰╯˘灬)',
icon: 'https://assets.misskey-hub.net/patrons/676eea72d4884d3f89aababbb62533fb.jpg',
}, {
name: '貯水よび',
icon: 'https://assets.misskey-hub.net/patrons/2974506d53244bbe94a67707b27099e2.jpg',
}, {
name: 'はるかさ',
icon: 'https://assets.misskey-hub.net/patrons/26ce2432739a400aa3aa0de0ef67a107.jpg',
}, {
name: '天鈴のあ',
icon: 'https://assets.misskey-hub.net/patrons/995cdbb00bd6421184461a883adfe1d9.jpg',
}, {
name: 'えとゔぁす',
icon: 'https://assets.misskey-hub.net/patrons/2578f441b82a44cfaa55ba83a318b26e.jpg',
}]; }];
const patrons = [ const patrons = [
@ -347,6 +362,7 @@ const patrons = [
'SHO SEKIGUCHI', 'SHO SEKIGUCHI',
'塩キャベツ', '塩キャベツ',
'はとぽぷさん', 'はとぽぷさん',
'100の人 (エスパー・イーシア)',
]; ];
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure')); const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));

View file

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local"> <MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local">
<template #label>{{ i18n.ts.expirationDate }}</template> <template #label>{{ i18n.ts.expirationDate }}</template>
</MkInput> </MkInput>
<MkInput v-model="createCount" type="number"> <MkInput v-model="createCount" type="number" min="1">
<template #label>{{ i18n.ts.createCount }}</template> <template #label>{{ i18n.ts.createCount }}</template>
</MkInput> </MkInput>
<MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton> <MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton>

View file

@ -93,7 +93,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { PageHeaderItem } from '@/types/page-header.js'; import { PageHeaderItem } from '@/types/page-header.js';
import { isSupportShare } from '@/scripts/navigator.js'; import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';

View file

@ -43,7 +43,7 @@ import { url } from '@/config.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { clipsCache } from '@/cache.js'; import { clipsCache } from '@/cache.js';
import { isSupportShare } from '@/scripts/navigator.js'; import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{ const props = defineProps<{
clipId: string, clipId: string,

View file

@ -210,7 +210,7 @@ import { apiUrl } from '@/config.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import MkRange from '@/components/MkRange.vue'; import MkRange from '@/components/MkRange.vue';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
type FrontendMonoDefinition = { type FrontendMonoDefinition = {
id: string; id: string;

View file

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { misskeyApiGet } from '@/scripts/misskey-api.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue'; import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';

View file

@ -37,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { AISCRIPT_VERSION } from '@syuilo/aiscript';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
@ -48,7 +49,7 @@ import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
const PRESET_DEFAULT = `/// @ 0.18.0 const PRESET_DEFAULT = `/// @ ${AISCRIPT_VERSION}
var name = "" var name = ""
@ -66,7 +67,7 @@ Ui:render([
]) ])
`; `;
const PRESET_OMIKUJI = `/// @ 0.18.0 const PRESET_OMIKUJI = `/// @ ${AISCRIPT_VERSION}
// //
// //
@ -109,7 +110,7 @@ Ui:render([
]) ])
`; `;
const PRESET_SHUFFLE = `/// @ 0.18.0 const PRESET_SHUFFLE = `/// @ ${AISCRIPT_VERSION}
// //
let string = "ペペロンチーノ" let string = "ペペロンチーノ"
@ -188,7 +189,7 @@ var cursor = 0
do() do()
`; `;
const PRESET_QUIZ = `/// @ 0.18.0 const PRESET_QUIZ = `/// @ ${AISCRIPT_VERSION}
let title = '地理クイズ' let title = '地理クイズ'
let qas = [{ let qas = [{
@ -301,7 +302,7 @@ qaEls.push(Ui:C:container({
Ui:render(qaEls) Ui:render(qaEls)
`; `;
const PRESET_TIMELINE = `/// @ 0.18.0 const PRESET_TIMELINE = `/// @ ${AISCRIPT_VERSION}
// API // API
@fetch() { @fetch() {

View file

@ -78,7 +78,7 @@ import MkCode from '@/components/MkCode.vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js'; import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { pleaseLogin } from '@/scripts/please-login.js'; import { pleaseLogin } from '@/scripts/please-login.js';
const props = defineProps<{ const props = defineProps<{

View file

@ -77,7 +77,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js'; import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
const router = useRouter(); const router = useRouter();

Some files were not shown because too many files have changed in this diff Show more