mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-29 09:58:36 +01:00
Merge branch 'develop' into feature/emoji-grid
This commit is contained in:
commit
7e0343d724
129 changed files with 1391 additions and 687 deletions
|
@ -1,5 +1,11 @@
|
|||
# misskey settings
|
||||
# MISSKEY_URL=https://example.tld/
|
||||
|
||||
# db settings
|
||||
POSTGRES_PASSWORD=example-misskey-pass
|
||||
# DATABASE_PASSWORD=${POSTGRES_PASSWORD}
|
||||
POSTGRES_USER=example-misskey-user
|
||||
# DATABASE_USER=${POSTGRES_USER}
|
||||
POSTGRES_DB=misskey
|
||||
# DATABASE_DB=${POSTGRES_DB}
|
||||
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#───┘ URL └─────────────────────────────────────────────────────
|
||||
|
||||
# Final accessible URL seen by a user.
|
||||
# You can set url from an environment variable instead.
|
||||
url: https://example.tld/
|
||||
|
||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
|
@ -38,9 +39,11 @@ db:
|
|||
port: 5432
|
||||
|
||||
# Database name
|
||||
# You can set db from an environment variable instead.
|
||||
db: misskey
|
||||
|
||||
# Auth
|
||||
# You can set user and pass from environment variables instead.
|
||||
user: example-misskey-user
|
||||
pass: example-misskey-pass
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ node_modules/
|
|||
packages/*/node_modules
|
||||
redis/
|
||||
files/
|
||||
misskey-assets/
|
||||
fluent-emojis/
|
||||
.pnp.*
|
||||
|
||||
|
|
8
.github/ISSUE_TEMPLATE/01_bug-report.yml
vendored
8
.github/ISSUE_TEMPLATE/01_bug-report.yml
vendored
|
@ -53,8 +53,8 @@ body:
|
|||
Examples:
|
||||
* Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4
|
||||
* Browser: Chrome 113.0.5672.126
|
||||
* Server URL: misskey.io
|
||||
* Misskey: 13.x.x
|
||||
* Server URL: misskey.example.com
|
||||
* Misskey: 2024.x.x
|
||||
value: |
|
||||
* Model and OS of the device(s):
|
||||
* Browser:
|
||||
|
@ -74,11 +74,11 @@ body:
|
|||
|
||||
Examples:
|
||||
* 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
|
||||
* PostgreSQL: 15.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: |
|
||||
* Installation Method or Hosting Service:
|
||||
* Misskey:
|
||||
|
|
2
.github/workflows/api-misskey-js.yml
vendored
2
.github/workflows/api-misskey-js.yml
vendored
|
@ -21,7 +21,7 @@ jobs:
|
|||
- run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
|
2
.github/workflows/changelog-check.yml
vendored
2
.github/workflows/changelog-check.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
|||
- name: Checkout head
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
|
||||
- name: setup node
|
||||
id: setup-node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: pnpm
|
||||
|
|
2
.github/workflows/get-api-diff.yml
vendored
2
.github/workflows/get-api-diff.yml
vendored
|
@ -34,7 +34,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
17
.github/workflows/lint.yml
vendored
17
.github/workflows/lint.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
|||
fetch-depth: 0
|
||||
submodules: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
@ -40,6 +40,8 @@ jobs:
|
|||
needs: [pnpm_install]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
env:
|
||||
eslint-cache-version: v1
|
||||
strategy:
|
||||
matrix:
|
||||
workspace:
|
||||
|
@ -53,13 +55,20 @@ jobs:
|
|||
fetch-depth: 0
|
||||
submodules: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: corepack enable
|
||||
- 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:
|
||||
needs: [pnpm_install]
|
||||
|
@ -76,7 +85,7 @@ jobs:
|
|||
fetch-depth: 0
|
||||
submodules: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
|
2
.github/workflows/locale.yml
vendored
2
.github/workflows/locale.yml
vendored
|
@ -19,7 +19,7 @@ jobs:
|
|||
fetch-depth: 0
|
||||
submodules: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
- uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
|
2
.github/workflows/on-release-created.yml
vendored
2
.github/workflows/on-release-created.yml
vendored
|
@ -26,7 +26,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
2
.github/workflows/storybook.yml
vendored
2
.github/workflows/storybook.yml
vendored
|
@ -36,7 +36,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
|
4
.github/workflows/test-backend.yml
vendored
4
.github/workflows/test-backend.yml
vendored
|
@ -46,7 +46,7 @@ jobs:
|
|||
- name: Install FFmpeg
|
||||
uses: FedericoCarboni/setup-ffmpeg@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
@ -93,7 +93,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
4
.github/workflows/test-frontend.yml
vendored
4
.github/workflows/test-frontend.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
@ -90,7 +90,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
2
.github/workflows/test-misskey-js.yml
vendored
2
.github/workflows/test-misskey-js.yml
vendored
|
@ -31,7 +31,7 @@ jobs:
|
|||
- run: corepack enable
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
2
.github/workflows/test-production.yml
vendored
2
.github/workflows/test-production.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
2
.github/workflows/validate-api-json.yml
vendored
2
.github/workflows/validate-api-json.yml
vendored
|
@ -27,7 +27,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -59,6 +59,7 @@ ormconfig.json
|
|||
temp
|
||||
/packages/frontend/src/**/*.stories.ts
|
||||
tsdoc-metadata.json
|
||||
misskey-assets
|
||||
|
||||
# blender backups
|
||||
*.blend1
|
||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,6 +1,3 @@
|
|||
[submodule "misskey-assets"]
|
||||
path = misskey-assets
|
||||
url = https://github.com/misskey-dev/assets.git
|
||||
[submodule "fluent-emojis"]
|
||||
path = fluent-emojis
|
||||
url = https://github.com/misskey-dev/emojis.git
|
||||
|
|
29
CHANGELOG.md
29
CHANGELOG.md
|
@ -1,7 +1,8 @@
|
|||
## Unreleased
|
||||
## 2024.7.0
|
||||
|
||||
### Note
|
||||
- デッキUIの新着ノートをサウンドで通知する機能の追加(v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
|
||||
- Streaming APIにて入力が不正な場合にはそのメッセージを無視するようになりました。 #14251
|
||||
|
||||
### General
|
||||
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
|
||||
|
@ -21,6 +22,9 @@
|
|||
(Based on https://github.com/taiyme/misskey/pull/226)
|
||||
- Enhance: サーバー情報ページ・お問い合わせページを改善
|
||||
(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: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
|
||||
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
|
||||
|
@ -29,6 +33,16 @@
|
|||
- Fix: テーマプレビューが見れない問題を修正
|
||||
- Fix: ショートカットキーが連打できる問題を修正
|
||||
(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
|
||||
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
|
||||
|
@ -38,7 +52,8 @@
|
|||
- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
|
||||
- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
|
||||
- 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: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
|
||||
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
|
||||
|
@ -53,8 +68,18 @@
|
|||
2. フォロー中かつ非アクティブなユーザ
|
||||
3. フォローしていないアクティブなユーザ
|
||||
4. フォローしていない非アクティブなユーザ
|
||||
|
||||
また、自分自身のアカウントもサジェストされるようになりました。
|
||||
- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正
|
||||
(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
|
||||
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# 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.
|
||||
|
||||
> **Note**
|
||||
> [!NOTE]
|
||||
> 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.\
|
||||
> 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.
|
||||
- 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.
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
- **`master`** branch is tracking the latest release and used for production purposes.
|
||||
|
@ -77,7 +92,7 @@ An actual domain will be assigned so you can test the federation.
|
|||
|
||||
## Release
|
||||
### 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.
|
||||
- Into `master` from `develop` branch.
|
||||
- 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 tag name must be the version
|
||||
|
||||
> **Note**
|
||||
> [!NOTE]
|
||||
> Why this instruction is necessary:
|
||||
> - To perform final QA checks
|
||||
> - 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)
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
command.
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
- Test codes are located in [`/packages/backend/test`](/packages/backend/test).
|
||||
|
||||
|
@ -204,7 +229,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
|
|||
### ルート定義
|
||||
ルート定義は、以下の形式のオブジェクトの配列です。
|
||||
|
||||
``` ts
|
||||
```ts
|
||||
{
|
||||
name?: string;
|
||||
path: string;
|
||||
|
@ -217,7 +242,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
|
|||
}
|
||||
```
|
||||
|
||||
> **Warning**
|
||||
> [!WARNING]
|
||||
> 現状、ルートは定義された順に評価されます。
|
||||
> たとえば、`/foo/:id`ルート定義の次に`/foo/bar`ルート定義がされていた場合、後者がマッチすることはありません。
|
||||
|
||||
|
@ -279,7 +304,7 @@ export const Default = {
|
|||
parameters: {
|
||||
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.
|
||||
|
@ -517,7 +542,7 @@ https://github.com/misskey-dev/misskey/pull/10082
|
|||
|
||||
テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合:
|
||||
|
||||
```
|
||||
```ts
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
|
|
@ -17,6 +17,8 @@ services:
|
|||
networks:
|
||||
- internal_network
|
||||
- external_network
|
||||
# env_file:
|
||||
# - .config/docker.env
|
||||
volumes:
|
||||
- ./files:/misskey/files
|
||||
- ./.config:/misskey/.config:ro
|
||||
|
|
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
|
@ -5012,6 +5012,14 @@ export interface Locale extends ILocale {
|
|||
* もう一度お試しください。
|
||||
*/
|
||||
"tryAgain": string;
|
||||
/**
|
||||
* センシティブなメディアを表示するとき確認する
|
||||
*/
|
||||
"confirmWhenRevealingSensitiveMedia": string;
|
||||
/**
|
||||
* センシティブなメディアです。表示しますか?
|
||||
*/
|
||||
"sensitiveMediaRevealConfirm": string;
|
||||
"_delivery": {
|
||||
/**
|
||||
* 配信状態
|
||||
|
|
|
@ -1249,6 +1249,8 @@ noDescription: "説明文はありません"
|
|||
alwaysConfirmFollow: "フォローの際常に確認する"
|
||||
inquiry: "お問い合わせ"
|
||||
tryAgain: "もう一度お試しください。"
|
||||
confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
|
||||
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
|
||||
|
||||
_delivery:
|
||||
status: "配信状態"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 0179793ec891856d6f37a3be16ba4c22f67a81b5
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2024.5.0",
|
||||
"version": "2024.7.0-beta.2",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"packageManager": "pnpm@9.5.0",
|
||||
"workspaces": [
|
||||
"packages/frontend",
|
||||
"packages/backend",
|
||||
|
|
|
@ -4,7 +4,7 @@ import sharedConfig from '../shared/eslint.config.js';
|
|||
export default [
|
||||
...sharedConfig,
|
||||
{
|
||||
ignores: ['**/node_modules', 'built', '@types/**/*'],
|
||||
ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'],
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
|
|
|
@ -23,7 +23,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
|
|||
* 設定ファイルの型
|
||||
*/
|
||||
type Source = {
|
||||
url: string;
|
||||
url?: string;
|
||||
port?: number;
|
||||
socket?: string;
|
||||
chmodSocket?: string;
|
||||
|
@ -31,9 +31,9 @@ type Source = {
|
|||
db: {
|
||||
host: string;
|
||||
port: number;
|
||||
db: string;
|
||||
user: string;
|
||||
pass: string;
|
||||
db?: string;
|
||||
user?: string;
|
||||
pass?: string;
|
||||
disableCache?: boolean;
|
||||
extra?: { [x: string]: string };
|
||||
};
|
||||
|
@ -202,13 +202,17 @@ export function loadConfig(): Config {
|
|||
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
|
||||
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 host = url.host;
|
||||
const hostname = url.hostname;
|
||||
const scheme = url.protocol.replace(/:$/, '');
|
||||
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 ?
|
||||
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
|
||||
: null;
|
||||
|
@ -231,7 +235,7 @@ export function loadConfig(): Config {
|
|||
apiUrl: `${scheme}://${host}/api`,
|
||||
authUrl: `${scheme}://${host}/auth`,
|
||||
driveUrl: `${scheme}://${host}/files`,
|
||||
db: config.db,
|
||||
db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass },
|
||||
dbReplications: config.dbReplications,
|
||||
dbSlaves: config.dbSlaves,
|
||||
meilisearch: config.meilisearch,
|
||||
|
@ -259,7 +263,7 @@ export function loadConfig(): Config {
|
|||
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
|
||||
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
|
||||
proxyRemoteFiles: config.proxyRemoteFiles,
|
||||
signToActivityPubGet: config.signToActivityPubGet,
|
||||
signToActivityPubGet: config.signToActivityPubGet ?? true,
|
||||
mediaProxy: externalMediaProxy ?? internalMediaProxy,
|
||||
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
|
||||
videoThumbnailGenerator: config.videoThumbnailGenerator ?
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// dummy
|
||||
export const MAX_NOTE_TEXT_LENGTH = 3000;
|
||||
|
||||
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
|
||||
|
|
|
@ -61,6 +61,7 @@ import { UserFollowingService } from './UserFollowingService.js';
|
|||
import { UserKeypairService } from './UserKeypairService.js';
|
||||
import { UserListService } from './UserListService.js';
|
||||
import { UserMutingService } from './UserMutingService.js';
|
||||
import { UserRenoteMutingService } from './UserRenoteMutingService.js';
|
||||
import { UserSuspendService } from './UserSuspendService.js';
|
||||
import { UserAuthService } from './UserAuthService.js';
|
||||
import { VideoProcessingService } from './VideoProcessingService.js';
|
||||
|
@ -203,6 +204,7 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx
|
|||
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
||||
const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService', useExisting: UserRenoteMutingService };
|
||||
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
|
||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
||||
|
@ -350,6 +352,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
UserKeypairService,
|
||||
UserListService,
|
||||
UserMutingService,
|
||||
UserRenoteMutingService,
|
||||
UserSearchService,
|
||||
UserSuspendService,
|
||||
UserAuthService,
|
||||
|
@ -493,6 +496,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$UserKeypairService,
|
||||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserRenoteMutingService,
|
||||
$UserSearchService,
|
||||
$UserSuspendService,
|
||||
$UserAuthService,
|
||||
|
@ -637,6 +641,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
UserKeypairService,
|
||||
UserListService,
|
||||
UserMutingService,
|
||||
UserRenoteMutingService,
|
||||
UserSearchService,
|
||||
UserSuspendService,
|
||||
UserAuthService,
|
||||
|
@ -779,6 +784,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$UserKeypairService,
|
||||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserRenoteMutingService,
|
||||
$UserSearchService,
|
||||
$UserSuspendService,
|
||||
$UserAuthService,
|
||||
|
|
|
@ -209,6 +209,10 @@ type SerializedAll<T> = {
|
|||
[K in keyof T]: Serialized<T[K]>;
|
||||
};
|
||||
|
||||
type UndefinedAsNullAll<T> = {
|
||||
[K in keyof T]: T[K] extends undefined ? null : T[K];
|
||||
}
|
||||
|
||||
export interface InternalEventTypes {
|
||||
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
|
||||
userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
|
||||
|
@ -247,43 +251,45 @@ export interface InternalEventTypes {
|
|||
userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
|
||||
}
|
||||
|
||||
type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>;
|
||||
|
||||
// name/messages(spec) pairs dictionary
|
||||
export type GlobalEvents = {
|
||||
internal: {
|
||||
name: 'internal';
|
||||
payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>;
|
||||
payload: EventTypesToEventPayload<InternalEventTypes>;
|
||||
};
|
||||
broadcast: {
|
||||
name: 'broadcast';
|
||||
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
|
||||
payload: EventTypesToEventPayload<BroadcastTypes>;
|
||||
};
|
||||
main: {
|
||||
name: `mainStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>;
|
||||
payload: EventTypesToEventPayload<MainEventTypes>;
|
||||
};
|
||||
drive: {
|
||||
name: `driveStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>;
|
||||
payload: EventTypesToEventPayload<DriveEventTypes>;
|
||||
};
|
||||
note: {
|
||||
name: `noteStream:${MiNote['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
|
||||
payload: EventTypesToEventPayload<NoteStreamEventTypes>;
|
||||
};
|
||||
userList: {
|
||||
name: `userListStream:${MiUserList['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
|
||||
payload: EventTypesToEventPayload<UserListEventTypes>;
|
||||
};
|
||||
roleTimeline: {
|
||||
name: `roleTimelineStream:${MiRole['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>;
|
||||
payload: EventTypesToEventPayload<RoleTimelineEventTypes>;
|
||||
};
|
||||
antenna: {
|
||||
name: `antennaStream:${MiAntenna['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>;
|
||||
payload: EventTypesToEventPayload<AntennaEventTypes>;
|
||||
};
|
||||
admin: {
|
||||
name: `adminStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>;
|
||||
payload: EventTypesToEventPayload<AdminEventTypes>;
|
||||
};
|
||||
notes: {
|
||||
name: 'notesStream';
|
||||
|
@ -291,11 +297,11 @@ export type GlobalEvents = {
|
|||
};
|
||||
reversi: {
|
||||
name: `reversiStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>;
|
||||
payload: EventTypesToEventPayload<ReversiEventTypes>;
|
||||
};
|
||||
reversiGame: {
|
||||
name: `reversiGameStream:${MiReversiGame['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>;
|
||||
payload: EventTypesToEventPayload<ReversiGameEventTypes>;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -933,12 +933,15 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
|
||||
// 自分自身のHTL
|
||||
if (note.userHost == null) {
|
||||
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自分自身以外への返信
|
||||
if (isReply(note)) {
|
||||
|
|
|
@ -505,14 +505,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
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', {
|
||||
roleId: roleId,
|
||||
});
|
||||
}
|
||||
|
||||
if (moderator) {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
||||
this.moderationLogService.log(moderator, 'assignRole', {
|
||||
roleId: roleId,
|
||||
roleName: role.name,
|
||||
|
|
|
@ -279,9 +279,11 @@ export class UserFollowingService implements OnModuleInit {
|
|||
});
|
||||
|
||||
// 通知を作成
|
||||
if (follower.host === null) {
|
||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||
}, followee.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (alreadyFollowed) return;
|
||||
|
||||
|
|
52
packages/backend/src/core/UserRenoteMutingService.ts
Normal file
52
packages/backend/src/core/UserRenoteMutingService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -74,10 +74,10 @@ export class ApQuestionService {
|
|||
|
||||
//#region このサーバーに既に登録されているか
|
||||
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 });
|
||||
if (poll == null) throw new Error('Question is not registed');
|
||||
if (poll == null) throw new Error('Question is not registered');
|
||||
//#endregion
|
||||
|
||||
// resolve new Question object
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
*/
|
||||
|
||||
export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
|
||||
if (!note) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (userIds.has(note.userId) && !ignoreAuthor) {
|
||||
return true;
|
||||
}
|
||||
|
|
8
packages/backend/src/misc/json-value.ts
Normal file
8
packages/backend/src/misc/json-value.ts
Normal 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[];
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import type { PollVotesRepository, NotesRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
|
@ -24,6 +25,7 @@ export class EndedPollNotificationProcessorService {
|
|||
@Inject(DI.pollVotesRepository)
|
||||
private pollVotesRepository: PollVotesRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private notificationService: NotificationService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
|
@ -47,9 +49,12 @@ export class EndedPollNotificationProcessorService {
|
|||
const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])];
|
||||
|
||||
for (const userId of userIds) {
|
||||
const profile = await this.cacheService.userProfileCache.fetch(userId);
|
||||
if (profile.userHost === null) {
|
||||
this.notificationService.createNotification(userId, 'pollEnded', {
|
||||
noteId: note.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,6 +139,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
timelineConfig = [
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
`localTimelineWithReplyTo:${me.id}`,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -6,12 +6,11 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
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 { GetterService } from '@/server/api/GetterService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js";
|
||||
import type { RenoteMutingsRepository } from '@/models/_.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
@ -62,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
private getterService: GetterService,
|
||||
private idService: IdService,
|
||||
private userRenoteMutingService: UserRenoteMutingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const muter = me;
|
||||
|
@ -79,21 +78,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
|
||||
// Check if already muting
|
||||
const exist = await this.renoteMutingsRepository.findOneBy({
|
||||
const exist = await this.renoteMutingsRepository.exists({
|
||||
where: {
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist === true) {
|
||||
throw new ApiError(meta.errors.alreadyMuting);
|
||||
}
|
||||
|
||||
// Create mute
|
||||
await this.renoteMutingsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
} as MiRenoteMuting);
|
||||
await this.userRenoteMutingService.mute(muter, mutee);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RenoteMutingsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js";
|
||||
import type { RenoteMutingsRepository } from '@/models/_.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
@ -53,6 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
private getterService: GetterService,
|
||||
private userRenoteMutingService: UserRenoteMutingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const muter = me;
|
||||
|
@ -79,9 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
// Delete mute
|
||||
await this.renoteMutingsRepository.delete({
|
||||
id: exist.id,
|
||||
});
|
||||
await this.userRenoteMutingService.unmute([exist]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -74,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private roleService: RoleService,
|
||||
) {
|
||||
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
|
||||
if (!iAmModerator) {
|
||||
const user = await this.cacheService.findUserById(ps.userId);
|
||||
|
@ -85,7 +87,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
|
||||
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'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
|
@ -94,9 +103,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
|
||||
const reactions = await query
|
||||
const reactions = (await query
|
||||
.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 });
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import { MiFollowing, MiUserProfile } from '@/models/_.js';
|
||||
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import type { ChannelsService } from './ChannelsService.js';
|
||||
import type { EventEmitter } from 'events';
|
||||
import type Channel from './channel.js';
|
||||
|
@ -28,7 +29,7 @@ export default class Connection {
|
|||
private wsConnection: WebSocket.WebSocket;
|
||||
public subscriber: StreamEventEmitter;
|
||||
private channels: Channel[] = [];
|
||||
private subscribingNotes: any = {};
|
||||
private subscribingNotes: Partial<Record<string, number>> = {};
|
||||
private cachedNotes: Packed<'Note'>[] = [];
|
||||
public userProfile: MiUserProfile | null = null;
|
||||
public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
|
||||
|
@ -101,7 +102,7 @@ export default class Connection {
|
|||
*/
|
||||
@bindThis
|
||||
private async onWsConnectionMessage(data: WebSocket.RawData) {
|
||||
let obj: Record<string, any>;
|
||||
let obj: JsonObject;
|
||||
|
||||
try {
|
||||
obj = JSON.parse(data.toString());
|
||||
|
@ -111,6 +112,8 @@ export default class Connection {
|
|||
|
||||
const { type, body } = obj;
|
||||
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
|
||||
|
||||
switch (type) {
|
||||
case 'readNotification': this.onReadNotification(body); break;
|
||||
case 'subNote': this.onSubscribeNote(body); break;
|
||||
|
@ -151,7 +154,7 @@ export default class Connection {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private readNote(body: any) {
|
||||
private readNote(body: JsonObject) {
|
||||
const id = body.id;
|
||||
|
||||
const note = this.cachedNotes.find(n => n.id === id);
|
||||
|
@ -163,7 +166,7 @@ export default class Connection {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private onReadNotification(payload: any) {
|
||||
private onReadNotification(payload: JsonObject) {
|
||||
this.notificationService.readAllNotification(this.user!.id);
|
||||
}
|
||||
|
||||
|
@ -171,16 +174,14 @@ export default class Connection {
|
|||
* 投稿購読要求時
|
||||
*/
|
||||
@bindThis
|
||||
private onSubscribeNote(payload: any) {
|
||||
if (!payload.id) return;
|
||||
private onSubscribeNote(payload: JsonObject) {
|
||||
if (!payload.id || typeof payload.id !== 'string') return;
|
||||
|
||||
if (this.subscribingNotes[payload.id] == null) {
|
||||
this.subscribingNotes[payload.id] = 0;
|
||||
}
|
||||
const current = this.subscribingNotes[payload.id] ?? 0;
|
||||
const updated = current + 1;
|
||||
this.subscribingNotes[payload.id] = updated;
|
||||
|
||||
this.subscribingNotes[payload.id]++;
|
||||
|
||||
if (this.subscribingNotes[payload.id] === 1) {
|
||||
if (updated === 1) {
|
||||
this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
|
||||
}
|
||||
}
|
||||
|
@ -189,11 +190,14 @@ export default class Connection {
|
|||
* 投稿購読解除要求時
|
||||
*/
|
||||
@bindThis
|
||||
private onUnsubscribeNote(payload: any) {
|
||||
if (!payload.id) return;
|
||||
private onUnsubscribeNote(payload: JsonObject) {
|
||||
if (!payload.id || typeof payload.id !== 'string') return;
|
||||
|
||||
this.subscribingNotes[payload.id]--;
|
||||
if (this.subscribingNotes[payload.id] <= 0) {
|
||||
const current = this.subscribingNotes[payload.id];
|
||||
if (current == null) return;
|
||||
const updated = current - 1;
|
||||
this.subscribingNotes[payload.id] = updated;
|
||||
if (updated <= 0) {
|
||||
delete this.subscribingNotes[payload.id];
|
||||
this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage);
|
||||
}
|
||||
|
@ -212,17 +216,22 @@ export default class Connection {
|
|||
* チャンネル接続要求時
|
||||
*/
|
||||
@bindThis
|
||||
private onChannelConnectRequested(payload: any) {
|
||||
private onChannelConnectRequested(payload: JsonObject) {
|
||||
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
|
||||
private onChannelDisconnectRequested(payload: any) {
|
||||
private onChannelDisconnectRequested(payload: JsonObject) {
|
||||
const { id } = payload;
|
||||
if (typeof id !== 'string') return;
|
||||
this.disconnectChannel(id);
|
||||
}
|
||||
|
||||
|
@ -230,7 +239,7 @@ export default class Connection {
|
|||
* クライアントにメッセージ送信
|
||||
*/
|
||||
@bindThis
|
||||
public sendMessageToWs(type: string, payload: any) {
|
||||
public sendMessageToWs(type: string, payload: JsonObject) {
|
||||
this.wsConnection.send(JSON.stringify({
|
||||
type: type,
|
||||
body: payload,
|
||||
|
@ -241,7 +250,7 @@ export default class Connection {
|
|||
* チャンネルに接続
|
||||
*/
|
||||
@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);
|
||||
|
||||
if (channelService.requireCredential && this.user == null) {
|
||||
|
@ -288,7 +297,11 @@ export default class Connection {
|
|||
* @param data メッセージ
|
||||
*/
|
||||
@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);
|
||||
if (channel != null && channel.onMessage != null) {
|
||||
channel.onMessage(data.type, data.body);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
|||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||
import type Connection from './Connection.js';
|
||||
|
||||
/**
|
||||
|
@ -81,10 +82,12 @@ export default abstract class Channel {
|
|||
this.connection = connection;
|
||||
}
|
||||
|
||||
public send(payload: { type: string, body: JsonValue }): void
|
||||
public send(type: string, payload: JsonValue): void
|
||||
@bindThis
|
||||
public send(typeOrPayload: any, payload?: any) {
|
||||
const type = payload === undefined ? typeOrPayload.type : typeOrPayload;
|
||||
const body = payload === undefined ? typeOrPayload.body : payload;
|
||||
public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) {
|
||||
const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string);
|
||||
const body = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).body : payload;
|
||||
|
||||
this.connection.sendMessageToWs('channel', {
|
||||
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 onMessage?(type: string, body: any): void;
|
||||
public onMessage?(type: string, body: JsonValue): void;
|
||||
}
|
||||
|
||||
export type MiChannelService<T extends boolean> = {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class AdminChannel extends Channel {
|
||||
|
@ -14,7 +15,7 @@ class AdminChannel extends Channel {
|
|||
public static kind = 'read:admin:stream';
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
// Subscribe admin stream
|
||||
this.subscriber.on(`adminStream:${this.user!.id}`, data => {
|
||||
this.send(data);
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class AntennaChannel extends Channel {
|
||||
|
@ -27,8 +28,9 @@ class AntennaChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.antennaId = params.antennaId as string;
|
||||
public async init(params: JsonObject) {
|
||||
if (typeof params.antennaId !== 'string') return;
|
||||
this.antennaId = params.antennaId;
|
||||
|
||||
// Subscribe stream
|
||||
this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
|
||||
|
|
|
@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class ChannelChannel extends Channel {
|
||||
|
@ -27,8 +28,9 @@ class ChannelChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.channelId = params.channelId as string;
|
||||
public async init(params: JsonObject) {
|
||||
if (typeof params.channelId !== 'string') return;
|
||||
this.channelId = params.channelId;
|
||||
|
||||
// Subscribe stream
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class DriveChannel extends Channel {
|
||||
|
@ -14,7 +15,7 @@ class DriveChannel extends Channel {
|
|||
public static kind = 'read:account';
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
// Subscribe drive stream
|
||||
this.subscriber.on(`driveStream:${this.user!.id}`, data => {
|
||||
this.send(data);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class GlobalTimelineChannel extends Channel {
|
||||
|
@ -32,12 +33,12 @@ class GlobalTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.gtlAvailable) return;
|
||||
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.withRenotes = !!(params.withRenotes ?? true);
|
||||
this.withFiles = !!(params.withFiles ?? false);
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class HashtagChannel extends Channel {
|
||||
|
@ -28,11 +29,11 @@ class HashtagChannel extends Channel {
|
|||
}
|
||||
|
||||
@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;
|
||||
|
||||
if (this.q == null) return;
|
||||
|
||||
// Subscribe stream
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class HomeTimelineChannel extends Channel {
|
||||
|
@ -29,9 +30,9 @@ class HomeTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
public async init(params: JsonObject) {
|
||||
this.withRenotes = !!(params.withRenotes ?? true);
|
||||
this.withFiles = !!(params.withFiles ?? false);
|
||||
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class HybridTimelineChannel extends Channel {
|
||||
|
@ -34,13 +35,13 @@ class HybridTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
@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);
|
||||
if (!policies.ltlAvailable) return;
|
||||
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.withRenotes = !!(params.withRenotes ?? true);
|
||||
this.withReplies = !!(params.withReplies ?? false);
|
||||
this.withFiles = !!(params.withFiles ?? false);
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class LocalTimelineChannel extends Channel {
|
||||
|
@ -33,13 +34,13 @@ class LocalTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.ltlAvailable) return;
|
||||
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.withRenotes = !!(params.withRenotes ?? true);
|
||||
this.withReplies = !!(params.withReplies ?? false);
|
||||
this.withFiles = !!(params.withFiles ?? false);
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
|
|||
import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class MainChannel extends Channel {
|
||||
|
@ -25,7 +26,7 @@ class MainChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
// Subscribe main stream channel
|
||||
this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
|
||||
switch (data.type) {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import Xev from 'xev';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
const ev = new Xev();
|
||||
|
@ -22,19 +23,22 @@ class QueueStatsChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
ev.addListener('queueStats', this.onStats);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private onStats(stats: any) {
|
||||
private onStats(stats: JsonObject) {
|
||||
this.send('stats', stats);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
public onMessage(type: string, body: JsonValue) {
|
||||
switch (type) {
|
||||
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 => {
|
||||
this.send('statsLog', statsLog);
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class ReversiGameChannel extends Channel {
|
||||
|
@ -28,25 +29,41 @@ class ReversiGameChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.gameId = params.gameId as string;
|
||||
public async init(params: JsonObject) {
|
||||
if (typeof params.gameId !== 'string') return;
|
||||
this.gameId = params.gameId;
|
||||
|
||||
this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
public onMessage(type: string, body: JsonValue) {
|
||||
switch (type) {
|
||||
case 'ready': this.ready(body); break;
|
||||
case 'updateSettings': this.updateSettings(body.key, body.value); break;
|
||||
case 'cancel': this.cancelGame(); break;
|
||||
case 'putStone': this.putStone(body.pos, body.id); break;
|
||||
case 'ready':
|
||||
if (typeof body !== 'boolean') return;
|
||||
this.ready(body);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async updateSettings(key: string, value: any) {
|
||||
private async updateSettings(key: string, value: JsonObject) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.reversiService.updateSettings(this.gameId!, this.user, key, value);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class ReversiChannel extends Channel {
|
||||
|
@ -21,7 +22,7 @@ class ReversiChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
this.subscriber.on(`reversiStream:${this.user!.id}`, this.send);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class RoleTimelineChannel extends Channel {
|
||||
|
@ -28,8 +29,9 @@ class RoleTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.roleId = params.roleId as string;
|
||||
public async init(params: JsonObject) {
|
||||
if (typeof params.roleId !== 'string') return;
|
||||
this.roleId = params.roleId;
|
||||
|
||||
this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import Xev from 'xev';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
const ev = new Xev();
|
||||
|
@ -22,19 +23,20 @@ class ServerStatsChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
ev.addListener('serverStats', this.onStats);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private onStats(stats: any) {
|
||||
private onStats(stats: JsonObject) {
|
||||
this.send('stats', stats);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
public onMessage(type: string, body: JsonValue) {
|
||||
switch (type) {
|
||||
case 'requestLog':
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
|
||||
ev.once(`serverStatsLog:${body.id}`, statsLog => {
|
||||
this.send('statsLog', statsLog);
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class UserListChannel extends Channel {
|
||||
|
@ -36,10 +37,11 @@ class UserListChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.listId = params.listId as string;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
public async init(params: JsonObject) {
|
||||
if (typeof params.listId !== 'string') return;
|
||||
this.listId = params.listId;
|
||||
this.withFiles = !!(params.withFiles ?? false);
|
||||
this.withRenotes = !!(params.withRenotes ?? true);
|
||||
|
||||
// Check existence and owner
|
||||
const listExist = await this.userListsRepository.exists({
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
import * as assert from 'assert';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Redis } from 'ioredis';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
|
||||
function genHost() {
|
||||
return randomString() + '.example.com';
|
||||
|
@ -492,6 +492,44 @@ describe('Timelines', () => {
|
|||
|
||||
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', () => {
|
||||
|
|
|
@ -53,7 +53,6 @@ await fs.readFile(
|
|||
'../../assets/**',
|
||||
'../../fluent-emojis/**',
|
||||
'../../locales/ja-JP.yml',
|
||||
'../../misskey-assets/**',
|
||||
'assets/**',
|
||||
'public/**',
|
||||
'../../pnpm-lock.yaml',
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "5.0.7",
|
||||
"@rollup/pluginutils": "5.1.0",
|
||||
"@syuilo/aiscript": "0.18.0",
|
||||
"@syuilo/aiscript": "0.19.0",
|
||||
"@tabler/icons-webfont": "3.3.0",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"@vitejs/plugin-vue": "5.0.5",
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { createApp, defineAsyncComponent, markRaw } from 'vue';
|
||||
import { common } from './common.js';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import { ui } from '@/config.js';
|
||||
import { i18n } from '@/i18n.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;
|
||||
if (announcement.display === 'dialog') {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
|
||||
|
@ -122,7 +123,9 @@ export async function mainBoot() {
|
|||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stream.on('announcementCreated', onAnnouncementCreated);
|
||||
|
||||
if ($i.isDeleted) {
|
||||
alert({
|
||||
|
@ -315,6 +318,9 @@ export async function mainBoot() {
|
|||
updateAccount({ hasUnreadAnnouncement: false });
|
||||
});
|
||||
|
||||
// 個人宛てお知らせが発行されたとき
|
||||
main.on('announcementCreated', onAnnouncementCreated);
|
||||
|
||||
// トークンが再生成されたとき
|
||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||
main.on('myTokenRegenerated', () => {
|
||||
|
|
|
@ -5,9 +5,12 @@
|
|||
|
||||
import { createApp, defineAsyncComponent } from 'vue';
|
||||
import { common } from './common.js';
|
||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||
|
||||
export async function subBoot() {
|
||||
const { isClientUpdated } = await common(() => createApp(
|
||||
defineAsyncComponent(() => import('@/ui/minimum.vue')),
|
||||
));
|
||||
|
||||
emojiPicker.init();
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import * as os from '@/os.js';
|
|||
import MkLoading from '@/components/global/MkLoading.vue';
|
||||
import { defaultStore } from '@/store.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<{
|
||||
code: string;
|
||||
|
|
|
@ -39,7 +39,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.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';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
|
@ -151,22 +151,26 @@ function drawImage(bitmap: CanvasImageSource) {
|
|||
}
|
||||
|
||||
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');
|
||||
if (!ctx) return;
|
||||
|
||||
// avgColorでお茶をにごす
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888';
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
|
||||
}
|
||||
|
||||
async function draw() {
|
||||
if (props.hash == null) return;
|
||||
if (import.meta.env.MODE === 'test' && props.hash == null) return;
|
||||
|
||||
drawAvg();
|
||||
|
||||
if (props.hash == null) return;
|
||||
|
||||
if (props.onlyAvgColor) return;
|
||||
|
||||
const work = await canvasPromise;
|
||||
|
|
|
@ -62,7 +62,7 @@ import { computed } from 'vue';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import MkFolder from '@/components/MkFolder.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 * as os from '@/os.js';
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
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 { i18n } from '@/i18n.js';
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@contextmenu.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">
|
||||
<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>
|
||||
|
@ -156,6 +156,18 @@ const audioEl = shallowRef<HTMLAudioElement>();
|
|||
// 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'));
|
||||
|
||||
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
|
||||
const menuShowing = ref(false);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div :class="$style.root">
|
||||
<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>
|
||||
<b>{{ i18n.ts.sensitive }}</b>
|
||||
<span>{{ i18n.ts.clickToShow }}</span>
|
||||
|
@ -24,24 +24,30 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef, watch, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkMediaAudio from '@/components/MkMediaAudio.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
const props = defineProps<{
|
||||
media: Misskey.entities.DriveFile;
|
||||
}>(), {
|
||||
});
|
||||
}>();
|
||||
|
||||
const audioEl = shallowRef<HTMLAudioElement>();
|
||||
const hide = ref(true);
|
||||
|
||||
watch(audioEl, () => {
|
||||
if (audioEl.value) {
|
||||
audioEl.value.volume = 0.3;
|
||||
async function show() {
|
||||
if (props.media.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.ts.sensitiveMediaRevealConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
});
|
||||
|
||||
hide.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -83,11 +83,21 @@ const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
|
|||
: props.image.thumbnailUrl,
|
||||
);
|
||||
|
||||
function onclick() {
|
||||
async function onclick(ev: MouseEvent) {
|
||||
if (!props.controls) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -138,15 +138,13 @@ onMounted(() => {
|
|||
pswpModule: PhotoSwipe,
|
||||
});
|
||||
|
||||
lightbox.on('itemData', (ev) => {
|
||||
const { itemData } = ev;
|
||||
|
||||
lightbox.addFilter('itemData', (itemData) => {
|
||||
// element is children
|
||||
const { element } = itemData;
|
||||
|
||||
const id = element?.dataset.id;
|
||||
const file = props.mediaList.find(media => media.id === id);
|
||||
if (!file) return;
|
||||
if (!file) return itemData;
|
||||
|
||||
itemData.src = file.url;
|
||||
itemData.w = Number(file.properties.width);
|
||||
|
@ -158,6 +156,8 @@ onMounted(() => {
|
|||
itemData.alt = file.comment ?? file.name;
|
||||
itemData.comment = file.comment ?? file.name;
|
||||
itemData.thumbCropped = true;
|
||||
|
||||
return itemData;
|
||||
});
|
||||
|
||||
lightbox.on('uiRegister', () => {
|
||||
|
|
|
@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@contextmenu.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">
|
||||
<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>
|
||||
|
@ -176,6 +176,18 @@ function hasFocus() {
|
|||
// 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'));
|
||||
|
||||
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
|
||||
const menuShowing = ref(false);
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
}"
|
||||
:style="{
|
||||
width: (width && !asDrawer) ? `${width}px` : '',
|
||||
maxHeight: maxHeight ? `${maxHeight}px` : '',
|
||||
maxHeight: maxHeight ? `min(${maxHeight}px, calc(100dvh - 32px))` : 'calc(100dvh - 32px)',
|
||||
}"
|
||||
@keydown.stop="() => {}"
|
||||
@contextmenu.self.prevent="() => {}"
|
||||
|
|
|
@ -33,7 +33,7 @@ import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue'
|
|||
import RouterView from '@/components/global/RouterView.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
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 { useScrollPositionManager } from '@/nirax.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
|
@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
</section>
|
||||
<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>
|
||||
</MkInput>
|
||||
<MkSelect v-model="unit" small>
|
||||
|
|
|
@ -81,6 +81,7 @@ function getReactionName(reaction: string): string {
|
|||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
line-height: 24px;
|
||||
padding-top: 4px;
|
||||
white-space: nowrap;
|
||||
|
|
|
@ -87,7 +87,7 @@ const host = ref(toUnicode(configHost));
|
|||
const totpLogin = ref(false);
|
||||
const isBackupCode = ref(false);
|
||||
const queryingKey = ref(false);
|
||||
const credentialRequest = ref<CredentialRequestOptions | null>(null);
|
||||
let credentialRequest: CredentialRequestOptions | null = null;
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'login', v: any): void;
|
||||
|
@ -122,14 +122,14 @@ function onLogin(res: any): Promise<void> | void {
|
|||
}
|
||||
|
||||
async function queryKey(): Promise<void> {
|
||||
if (credentialRequest.value == null) return;
|
||||
if (credentialRequest == null) return;
|
||||
queryingKey.value = true;
|
||||
await webAuthnRequest(credentialRequest.value)
|
||||
await webAuthnRequest(credentialRequest)
|
||||
.catch(() => {
|
||||
queryingKey.value = false;
|
||||
return Promise.reject(null);
|
||||
}).then(credential => {
|
||||
credentialRequest.value = null;
|
||||
credentialRequest = null;
|
||||
queryingKey.value = false;
|
||||
signing.value = true;
|
||||
return misskeyApi('signin', {
|
||||
|
@ -160,7 +160,7 @@ function onSubmit(): void {
|
|||
}).then(res => {
|
||||
totpLogin.value = true;
|
||||
signing.value = false;
|
||||
credentialRequest.value = parseRequestOptionsFromJSON({
|
||||
credentialRequest = parseRequestOptionsFromJSON({
|
||||
publicKey: res,
|
||||
});
|
||||
})
|
||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
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(';')"
|
||||
: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 }"
|
||||
></iframe>
|
||||
<span v-else>invalid url</span>
|
||||
|
@ -91,6 +91,7 @@ import * as os from '@/os.js';
|
|||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { versatileLang } from '@/scripts/intl-const.js';
|
||||
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
type SummalyResult = Awaited<ReturnType<typeof summaly>>;
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<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/>
|
||||
<div :class="$style.title">
|
||||
<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 { $i } from '@/account.js';
|
||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
defineProps<{
|
||||
user: Misskey.entities.UserDetailed;
|
||||
|
|
|
@ -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="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>
|
||||
</div>
|
||||
<svg viewBox="0 0 128 128" :class="$style.avatarBack">
|
||||
|
@ -67,6 +67,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
|
||||
const props = defineProps<{
|
||||
showing: boolean;
|
||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="poamfof">
|
||||
<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<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>
|
||||
<span v-else>invalid url</span>
|
||||
</Transition>
|
||||
|
@ -27,6 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { ref } from 'vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { versatileLang } from '@/scripts/intl-const.js';
|
||||
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
@ -16,7 +16,7 @@ export type MkABehavior = 'window' | 'browser' | null;
|
|||
<script lang="ts" setup>
|
||||
import { computed, inject, shallowRef } from 'vue';
|
||||
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 { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
|
|
@ -31,7 +31,7 @@ import { defaultStore } from '@/store.js';
|
|||
import { customEmojisMap } from '@/custom-emojis.js';
|
||||
import * as os from '@/os.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 { i18n } from '@/i18n.js';
|
||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||
|
|
|
@ -14,7 +14,7 @@ import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@/scripts/emoji-
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.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 { i18n } from '@/i18n.js';
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
const validTime = (t: string | boolean | null | undefined) => {
|
||||
if (t == null) 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 => {
|
||||
|
|
|
@ -53,7 +53,7 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
|
|||
const current = resolveNested(router.current)!;
|
||||
const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
|
||||
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 }) {
|
||||
const current = resolveNested(resolved);
|
||||
|
|
|
@ -22,7 +22,7 @@ import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
|
|||
import MkPopupMenu from '@/components/MkPopupMenu.vue';
|
||||
import MkContextMenu from '@/components/MkContextMenu.vue';
|
||||
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 { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||
|
|
|
@ -243,6 +243,21 @@ const patronsWithIcon = [{
|
|||
}, {
|
||||
name: '越貝鯛丸',
|
||||
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 = [
|
||||
|
@ -347,6 +362,7 @@ const patrons = [
|
|||
'SHO SEKIGUCHI',
|
||||
'塩キャベツ',
|
||||
'はとぽぷさん',
|
||||
'100の人 (エスパー・イーシア)',
|
||||
];
|
||||
|
||||
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local">
|
||||
<template #label>{{ i18n.ts.expirationDate }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="createCount" type="number">
|
||||
<MkInput v-model="createCount" type="number" min="1">
|
||||
<template #label>{{ i18n.ts.createCount }}</template>
|
||||
</MkInput>
|
||||
<MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton>
|
||||
|
|
|
@ -93,7 +93,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
|||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { PageHeaderItem } from '@/types/page-header.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 { useRouter } from '@/router/supplier.js';
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ import { url } from '@/config.js';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import { clipsCache } from '@/cache.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<{
|
||||
clipId: string,
|
||||
|
|
|
@ -210,7 +210,7 @@ import { apiUrl } from '@/config.js';
|
|||
import { $i } from '@/account.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import MkRange from '@/components/MkRange.vue';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
|
||||
type FrontendMonoDefinition = {
|
||||
id: string;
|
||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.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 MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { AISCRIPT_VERSION } from '@syuilo/aiscript';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.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 { useRouter } from '@/router/supplier.js';
|
||||
|
||||
const PRESET_DEFAULT = `/// @ 0.18.0
|
||||
const PRESET_DEFAULT = `/// @ ${AISCRIPT_VERSION}
|
||||
|
||||
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 = "ペペロンチーノ"
|
||||
|
@ -188,7 +189,7 @@ var cursor = 0
|
|||
do()
|
||||
`;
|
||||
|
||||
const PRESET_QUIZ = `/// @ 0.18.0
|
||||
const PRESET_QUIZ = `/// @ ${AISCRIPT_VERSION}
|
||||
let title = '地理クイズ'
|
||||
|
||||
let qas = [{
|
||||
|
@ -301,7 +302,7 @@ qaEls.push(Ui:C:container({
|
|||
Ui:render(qaEls)
|
||||
`;
|
||||
|
||||
const PRESET_TIMELINE = `/// @ 0.18.0
|
||||
const PRESET_TIMELINE = `/// @ ${AISCRIPT_VERSION}
|
||||
// APIリクエストを行いローカルタイムラインを表示するプリセット
|
||||
|
||||
@fetch() {
|
||||
|
|
|
@ -78,7 +78,7 @@ import MkCode from '@/components/MkCode.vue';
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { $i } from '@/account.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';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
@ -77,7 +77,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { $i } from '@/account.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';
|
||||
|
||||
const router = useRouter();
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue