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
151b6c7d93
151 changed files with 3168 additions and 1569 deletions
3
.github/workflows/api-misskey-js.yml
vendored
3
.github/workflows/api-misskey-js.yml
vendored
|
@ -4,10 +4,11 @@ on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/api-misskey-js.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/api-misskey-js.yml
|
||||||
jobs:
|
jobs:
|
||||||
report:
|
report:
|
||||||
|
|
||||||
|
|
|
@ -6,12 +6,13 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- packages/misskey-js/package.json
|
- packages/misskey-js/package.json
|
||||||
- package.json
|
- package.json
|
||||||
|
- .github/workflows/check-misskey-js-version.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ develop ]
|
branches: [ develop ]
|
||||||
paths:
|
paths:
|
||||||
- packages/misskey-js/package.json
|
- packages/misskey-js/package.json
|
||||||
- package.json
|
- package.json
|
||||||
|
- .github/workflows/check-misskey-js-version.yml
|
||||||
jobs:
|
jobs:
|
||||||
check-version:
|
check-version:
|
||||||
# ルートの package.json と packages/misskey-js/package.json のバージョンが一致しているかを確認する
|
# ルートの package.json と packages/misskey-js/package.json のバージョンが一致しているかを確認する
|
||||||
|
|
2
.github/workflows/get-api-diff.yml
vendored
2
.github/workflows/get-api-diff.yml
vendored
|
@ -9,7 +9,7 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
- .github/workflows/get-api-diff.yml
|
- .github/workflows/get-api-diff.yml
|
||||||
|
- .github/workflows/get-api-diff.yml
|
||||||
jobs:
|
jobs:
|
||||||
get-from-misskey:
|
get-from-misskey:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
3
.github/workflows/lint.yml
vendored
3
.github/workflows/lint.yml
vendored
|
@ -11,6 +11,7 @@ on:
|
||||||
- packages/sw/**
|
- packages/sw/**
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
- packages/shared/eslint.config.js
|
- packages/shared/eslint.config.js
|
||||||
|
- .github/workflows/lint.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
|
@ -18,7 +19,7 @@ on:
|
||||||
- packages/sw/**
|
- packages/sw/**
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
- packages/shared/eslint.config.js
|
- packages/shared/eslint.config.js
|
||||||
|
- .github/workflows/lint.yml
|
||||||
jobs:
|
jobs:
|
||||||
pnpm_install:
|
pnpm_install:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
3
.github/workflows/locale.yml
vendored
3
.github/workflows/locale.yml
vendored
|
@ -4,10 +4,11 @@ on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- locales/**
|
- locales/**
|
||||||
|
- .github/workflows/locale.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- locales/**
|
- locales/**
|
||||||
|
- .github/workflows/locale.yml
|
||||||
jobs:
|
jobs:
|
||||||
locale_verify:
|
locale_verify:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
19
.github/workflows/release-edit-with-push.yml
vendored
19
.github/workflows/release-edit-with-push.yml
vendored
|
@ -3,10 +3,10 @@ name: "Release Manager: sync changelog with PR"
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- release/**
|
- develop
|
||||||
paths:
|
paths:
|
||||||
- 'CHANGELOG.md'
|
- 'CHANGELOG.md'
|
||||||
|
# - .github/workflows/release-edit-with-push.yml
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
@ -20,24 +20,29 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
# headがrelease/かつopenのPRを1つ取得
|
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
|
||||||
- name: Get PR
|
- name: Get PR
|
||||||
run: |
|
run: |
|
||||||
echo "pr_number=$(gh pr list --limit 1 --head "$GITHUB_REF_NAME" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
|
echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
|
||||||
id: get_pr
|
id: get_pr
|
||||||
|
env:
|
||||||
|
STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
|
||||||
- name: Get target version
|
- name: Get target version
|
||||||
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v1
|
if: steps.get_pr.outputs.pr_number != ''
|
||||||
|
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v2
|
||||||
id: v
|
id: v
|
||||||
# CHANGELOG.mdの内容を取得
|
# CHANGELOG.mdの内容を取得
|
||||||
- name: Get changelog
|
- name: Get changelog
|
||||||
uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v1
|
if: steps.get_pr.outputs.pr_number != ''
|
||||||
|
uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v2
|
||||||
with:
|
with:
|
||||||
version: ${{ steps.v.outputs.target_version }}
|
version: ${{ steps.v.outputs.target_version }}
|
||||||
id: changelog
|
id: changelog
|
||||||
# PRのnotesを更新
|
# PRのnotesを更新
|
||||||
- name: Update PR
|
- name: Update PR
|
||||||
|
if: steps.get_pr.outputs.pr_number != ''
|
||||||
run: |
|
run: |
|
||||||
gh pr edit "$PR_NUMBER" --body "$CHANGELOG"
|
gh pr edit "$PR_NUMBER" --body "$CHANGELOG"
|
||||||
env:
|
env:
|
||||||
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
|
|
||||||
PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }}
|
PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }}
|
||||||
|
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
|
||||||
|
|
20
.github/workflows/release-with-dispatch.yml
vendored
20
.github/workflows/release-with-dispatch.yml
vendored
|
@ -33,18 +33,21 @@ jobs:
|
||||||
pr_number: ${{ steps.get_pr.outputs.pr_number }}
|
pr_number: ${{ steps.get_pr.outputs.pr_number }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
# headがrelease/かつopenのPRを1つ取得
|
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
|
||||||
- name: Get PRs
|
- name: Get PRs
|
||||||
run: |
|
run: |
|
||||||
echo "pr_number=$(gh pr list --limit 1 --search "head:release/ is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
|
echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
|
||||||
id: get_pr
|
id: get_pr
|
||||||
|
env:
|
||||||
|
STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
|
||||||
|
|
||||||
merge:
|
merge:
|
||||||
uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v1
|
uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v2
|
||||||
needs: get-pr
|
needs: get-pr
|
||||||
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }}
|
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }}
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ needs.get-pr.outputs.pr_number }}
|
pr_number: ${{ needs.get-pr.outputs.pr_number }}
|
||||||
|
user: 'github-actions[bot]'
|
||||||
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
||||||
# Text to prepend to the changelog
|
# Text to prepend to the changelog
|
||||||
# The first line must be `## Unreleased`
|
# The first line must be `## Unreleased`
|
||||||
|
@ -65,15 +68,14 @@ jobs:
|
||||||
secrets:
|
secrets:
|
||||||
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
||||||
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||||
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
|
|
||||||
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
|
|
||||||
|
|
||||||
create-prerelease:
|
create-prerelease:
|
||||||
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
|
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
|
||||||
needs: get-pr
|
needs: get-pr
|
||||||
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }}
|
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }}
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ needs.get-pr.outputs.pr_number }}
|
pr_number: ${{ needs.get-pr.outputs.pr_number }}
|
||||||
|
user: 'github-actions[bot]'
|
||||||
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
||||||
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
||||||
indent: ${{ vars.INDENT }}
|
indent: ${{ vars.INDENT }}
|
||||||
|
@ -82,10 +84,11 @@ jobs:
|
||||||
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
create-target:
|
create-target:
|
||||||
uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v1
|
uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v2
|
||||||
needs: get-pr
|
needs: get-pr
|
||||||
if: ${{ needs.get-pr.outputs.pr_number == '' }}
|
if: ${{ needs.get-pr.outputs.pr_number == '' }}
|
||||||
with:
|
with:
|
||||||
|
user: 'github-actions[bot]'
|
||||||
# The script for version increment.
|
# The script for version increment.
|
||||||
# process.env.CURRENT_VERSION: The current version.
|
# process.env.CURRENT_VERSION: The current version.
|
||||||
#
|
#
|
||||||
|
@ -118,8 +121,7 @@ jobs:
|
||||||
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
||||||
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
||||||
indent: ${{ vars.INDENT }}
|
indent: ${{ vars.INDENT }}
|
||||||
|
stable_branch: ${{ vars.STABLE_BRANCH }}
|
||||||
secrets:
|
secrets:
|
||||||
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
||||||
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||||
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
|
|
||||||
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
|
|
||||||
|
|
13
.github/workflows/release-with-ready.yml
vendored
13
.github/workflows/release-with-ready.yml
vendored
|
@ -16,23 +16,26 @@ jobs:
|
||||||
check:
|
check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
ref: ${{ steps.get_pr.outputs.ref }}
|
head: ${{ steps.get_pr.outputs.head }}
|
||||||
|
base: ${{ steps.get_pr.outputs.base }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
# PR情報を取得
|
# PR情報を取得
|
||||||
- name: Get PR
|
- name: Get PR
|
||||||
run: |
|
run: |
|
||||||
pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName)
|
pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName,baseRefName)
|
||||||
echo "ref=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
|
echo "head=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
|
||||||
|
echo "base=$(echo $pr_json | jq -r '.baseRefName')" >> $GITHUB_OUTPUT
|
||||||
id: get_pr
|
id: get_pr
|
||||||
env:
|
env:
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
release:
|
release:
|
||||||
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
|
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
|
||||||
needs: check
|
needs: check
|
||||||
if: startsWith(needs.check.outputs.ref, 'release/')
|
if: needs.check.outputs.head == github.event.repository.default_branch && needs.check.outputs.base == vars.STABLE_BRANCH
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ github.event.pull_request.number }}
|
pr_number: ${{ github.event.pull_request.number }}
|
||||||
|
user: 'github-actions[bot]'
|
||||||
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
||||||
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
||||||
indent: ${{ vars.INDENT }}
|
indent: ${{ vars.INDENT }}
|
||||||
|
|
3
.github/workflows/test-backend.yml
vendored
3
.github/workflows/test-backend.yml
vendored
|
@ -9,12 +9,13 @@ on:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
# for permissions
|
# for permissions
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/test-backend.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
# for permissions
|
# for permissions
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/test-backend.yml
|
||||||
jobs:
|
jobs:
|
||||||
unit:
|
unit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
4
.github/workflows/test-frontend.yml
vendored
4
.github/workflows/test-frontend.yml
vendored
|
@ -11,7 +11,7 @@ on:
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
# for e2e
|
# for e2e
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
|
- .github/workflows/test-frontend.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- packages/frontend/**
|
- packages/frontend/**
|
||||||
|
@ -19,7 +19,7 @@ on:
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
# for e2e
|
# for e2e
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
|
- .github/workflows/test-frontend.yml
|
||||||
jobs:
|
jobs:
|
||||||
vitest:
|
vitest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
3
.github/workflows/test-misskey-js.yml
vendored
3
.github/workflows/test-misskey-js.yml
vendored
|
@ -8,11 +8,12 @@ on:
|
||||||
branches: [ develop ]
|
branches: [ develop ]
|
||||||
paths:
|
paths:
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/test-misskey-js.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ develop ]
|
branches: [ develop ]
|
||||||
paths:
|
paths:
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/test-misskey-js.yml
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|
||||||
|
|
3
.github/workflows/validate-api-json.yml
vendored
3
.github/workflows/validate-api-json.yml
vendored
|
@ -7,10 +7,11 @@ on:
|
||||||
- develop
|
- develop
|
||||||
paths:
|
paths:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
|
- .github/workflows/validate-api-json.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
|
- .github/workflows/validate-api-json.yml
|
||||||
jobs:
|
jobs:
|
||||||
validate-api-json:
|
validate-api-json:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -1,19 +1,34 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Note
|
||||||
|
- デッキUIの新着ノートをサウンドで通知する機能の追加(v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
|
||||||
|
|
||||||
### General
|
### General
|
||||||
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
|
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
|
||||||
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
|
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
|
||||||
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
|
- 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
|
||||||
- Feat: カスタム絵文字管理画面をリニューアル #10996
|
- Feat: カスタム絵文字管理画面をリニューアル #10996
|
||||||
* β版として公開のため、旧画面も引き続き利用可能です
|
* β版として公開のため、旧画面も引き続き利用可能です
|
||||||
|
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
|
||||||
|
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
|
||||||
|
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
|
||||||
|
- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加
|
||||||
|
- Enhance: 非ログイン時のハイライトTLのデザインを改善
|
||||||
|
- Enhance: フロントエンドのアクセシビリティ改善
|
||||||
|
(Based on https://github.com/taiyme/misskey/pull/226)
|
||||||
|
- Enhance: サーバー情報ページ・お問い合わせページを改善
|
||||||
|
(Cherry-picked from https://github.com/taiyme/misskey/pull/238)
|
||||||
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
|
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
|
||||||
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
|
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
|
||||||
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
|
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
|
||||||
- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
|
- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
|
||||||
- Fix: アンテナの編集画面のボタンに隙間を追加
|
- Fix: アンテナの編集画面のボタンに隙間を追加
|
||||||
- Fix: テーマプレビューが見れない問題を修正
|
- Fix: テーマプレビューが見れない問題を修正
|
||||||
|
- Fix: ショートカットキーが連打できる問題を修正
|
||||||
|
(Cherry-picked from https://github.com/taiyme/misskey/pull/234)
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
|
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
|
||||||
|
@ -33,9 +48,17 @@
|
||||||
- Fix: リノートにリアクションできないように
|
- Fix: リノートにリアクションできないように
|
||||||
- Fix: ユーザー名の前後に空白文字列がある場合は省略するように
|
- Fix: ユーザー名の前後に空白文字列がある場合は省略するように
|
||||||
- Fix: プロフィール編集時に名前を空白文字列のみにできる問題を修正
|
- Fix: プロフィール編集時に名前を空白文字列のみにできる問題を修正
|
||||||
|
- Fix: ユーザ名のサジェスト時に表示される内容と順番を調整(以下の順番になります) #14149
|
||||||
|
1. フォロー中かつアクティブなユーザ
|
||||||
|
2. フォロー中かつ非アクティブなユーザ
|
||||||
|
3. フォローしていないアクティブなユーザ
|
||||||
|
4. フォローしていない非アクティブなユーザ
|
||||||
|
- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正
|
||||||
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652)
|
||||||
|
|
||||||
### Misskey.js
|
### Misskey.js
|
||||||
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
|
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
|
||||||
|
- Feat: `/admin/role/create` のロールポリシーの型を修正
|
||||||
|
|
||||||
## 2024.5.0
|
## 2024.5.0
|
||||||
|
|
||||||
|
|
|
@ -165,7 +165,7 @@ cp .github/misskey/test.yml .config/
|
||||||
```
|
```
|
||||||
Prepare DB/Redis for testing.
|
Prepare DB/Redis for testing.
|
||||||
```
|
```
|
||||||
docker compose -f packages/backend/test/compose.yaml up
|
docker compose -f packages/backend/test/compose.yml up
|
||||||
```
|
```
|
||||||
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
|
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
|
||||||
|
|
||||||
|
|
38
locales/index.d.ts
vendored
38
locales/index.d.ts
vendored
|
@ -740,6 +740,22 @@ export interface Locale extends ILocale {
|
||||||
* リモートで表示
|
* リモートで表示
|
||||||
*/
|
*/
|
||||||
"showOnRemote": string;
|
"showOnRemote": string;
|
||||||
|
/**
|
||||||
|
* リモートで続行
|
||||||
|
*/
|
||||||
|
"continueOnRemote": string;
|
||||||
|
/**
|
||||||
|
* Misskey Hubからサーバーを選択
|
||||||
|
*/
|
||||||
|
"chooseServerOnMisskeyHub": string;
|
||||||
|
/**
|
||||||
|
* サーバーのドメインを直接指定
|
||||||
|
*/
|
||||||
|
"specifyServerHost": string;
|
||||||
|
/**
|
||||||
|
* ドメインを入力してください
|
||||||
|
*/
|
||||||
|
"inputHostName": string;
|
||||||
/**
|
/**
|
||||||
* 全般
|
* 全般
|
||||||
*/
|
*/
|
||||||
|
@ -1925,9 +1941,13 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"onlyOneFileCanBeAttached": string;
|
"onlyOneFileCanBeAttached": string;
|
||||||
/**
|
/**
|
||||||
* 続行する前に、サインアップまたはサインインが必要です
|
* 続行する前に、登録またはログインが必要です
|
||||||
*/
|
*/
|
||||||
"signinRequired": string;
|
"signinRequired": string;
|
||||||
|
/**
|
||||||
|
* 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります
|
||||||
|
*/
|
||||||
|
"signinOrContinueOnRemote": string;
|
||||||
/**
|
/**
|
||||||
* 招待
|
* 招待
|
||||||
*/
|
*/
|
||||||
|
@ -4988,6 +5008,10 @@ export interface Locale extends ILocale {
|
||||||
* お問い合わせ
|
* お問い合わせ
|
||||||
*/
|
*/
|
||||||
"inquiry": string;
|
"inquiry": string;
|
||||||
|
/**
|
||||||
|
* もう一度お試しください。
|
||||||
|
*/
|
||||||
|
"tryAgain": string;
|
||||||
"_delivery": {
|
"_delivery": {
|
||||||
/**
|
/**
|
||||||
* 配信状態
|
* 配信状態
|
||||||
|
@ -6598,6 +6622,10 @@ export interface Locale extends ILocale {
|
||||||
* ファイルにNSFWを常に付与
|
* ファイルにNSFWを常に付与
|
||||||
*/
|
*/
|
||||||
"alwaysMarkNsfw": string;
|
"alwaysMarkNsfw": string;
|
||||||
|
/**
|
||||||
|
* アイコンとバナーの更新を許可
|
||||||
|
*/
|
||||||
|
"canUpdateBioMedia": string;
|
||||||
/**
|
/**
|
||||||
* ノートのピン留めの最大数
|
* ノートのピン留めの最大数
|
||||||
*/
|
*/
|
||||||
|
@ -7519,14 +7547,6 @@ export interface Locale extends ILocale {
|
||||||
* 通知
|
* 通知
|
||||||
*/
|
*/
|
||||||
"notification": string;
|
"notification": string;
|
||||||
/**
|
|
||||||
* アンテナ受信
|
|
||||||
*/
|
|
||||||
"antenna": string;
|
|
||||||
/**
|
|
||||||
* チャンネル通知
|
|
||||||
*/
|
|
||||||
"channel": string;
|
|
||||||
/**
|
/**
|
||||||
* リアクション選択時
|
* リアクション選択時
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -181,6 +181,10 @@ addAccount: "アカウントを追加"
|
||||||
reloadAccountsList: "アカウントリストの情報を更新"
|
reloadAccountsList: "アカウントリストの情報を更新"
|
||||||
loginFailed: "ログインに失敗しました"
|
loginFailed: "ログインに失敗しました"
|
||||||
showOnRemote: "リモートで表示"
|
showOnRemote: "リモートで表示"
|
||||||
|
continueOnRemote: "リモートで続行"
|
||||||
|
chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
|
||||||
|
specifyServerHost: "サーバーのドメインを直接指定"
|
||||||
|
inputHostName: "ドメインを入力してください"
|
||||||
general: "全般"
|
general: "全般"
|
||||||
wallpaper: "壁紙"
|
wallpaper: "壁紙"
|
||||||
setWallpaper: "壁紙を設定"
|
setWallpaper: "壁紙を設定"
|
||||||
|
@ -477,7 +481,8 @@ attachAsFileQuestion: "クリップボードのテキストが長いです。テ
|
||||||
noMessagesYet: "まだチャットはありません"
|
noMessagesYet: "まだチャットはありません"
|
||||||
newMessageExists: "新しいメッセージがあります"
|
newMessageExists: "新しいメッセージがあります"
|
||||||
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
|
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
|
||||||
signinRequired: "続行する前に、サインアップまたはサインインが必要です"
|
signinRequired: "続行する前に、登録またはログインが必要です"
|
||||||
|
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
|
||||||
invitations: "招待"
|
invitations: "招待"
|
||||||
invitationCode: "招待コード"
|
invitationCode: "招待コード"
|
||||||
checking: "確認しています"
|
checking: "確認しています"
|
||||||
|
@ -1243,6 +1248,7 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
|
||||||
noDescription: "説明文はありません"
|
noDescription: "説明文はありません"
|
||||||
alwaysConfirmFollow: "フォローの際常に確認する"
|
alwaysConfirmFollow: "フォローの際常に確認する"
|
||||||
inquiry: "お問い合わせ"
|
inquiry: "お問い合わせ"
|
||||||
|
tryAgain: "もう一度お試しください。"
|
||||||
|
|
||||||
_delivery:
|
_delivery:
|
||||||
status: "配信状態"
|
status: "配信状態"
|
||||||
|
@ -1706,6 +1712,7 @@ _role:
|
||||||
canManageAvatarDecorations: "アバターデコレーションの管理"
|
canManageAvatarDecorations: "アバターデコレーションの管理"
|
||||||
driveCapacity: "ドライブ容量"
|
driveCapacity: "ドライブ容量"
|
||||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||||
|
canUpdateBioMedia: "アイコンとバナーの更新を許可"
|
||||||
pinMax: "ノートのピン留めの最大数"
|
pinMax: "ノートのピン留めの最大数"
|
||||||
antennaMax: "アンテナの作成可能数"
|
antennaMax: "アンテナの作成可能数"
|
||||||
wordMuteMax: "ワードミュートの最大文字数"
|
wordMuteMax: "ワードミュートの最大文字数"
|
||||||
|
@ -1972,8 +1979,6 @@ _sfx:
|
||||||
note: "ノート"
|
note: "ノート"
|
||||||
noteMy: "ノート(自分)"
|
noteMy: "ノート(自分)"
|
||||||
notification: "通知"
|
notification: "通知"
|
||||||
antenna: "アンテナ受信"
|
|
||||||
channel: "チャンネル通知"
|
|
||||||
reaction: "リアクション選択時"
|
reaction: "リアクション選択時"
|
||||||
|
|
||||||
_soundSettings:
|
_soundSettings:
|
||||||
|
|
20
packages/backend/assets/api-doc.html
Normal file
20
packages/backend/assets/api-doc.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Misskey API</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script
|
||||||
|
id="api-reference"
|
||||||
|
data-url="/api.json"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,24 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Misskey API</title>
|
|
||||||
<!-- needed for adaptive design -->
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
|
||||||
|
|
||||||
<!--
|
|
||||||
ReDoc doesn't change outer page styles
|
|
||||||
-->
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
|
|
||||||
<script src="https://cdn.redoc.ly/redoc/v2.1.3/bundles/redoc.standalone.js" integrity="sha256-u4DgqzYXoArvNF/Ymw3puKexfOC6lYfw0sfmeliBJ1I=" crossorigin="anonymous"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
|
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
|
||||||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||||
import { AccountMoveService } from './AccountMoveService.js';
|
import { AccountMoveService } from './AccountMoveService.js';
|
||||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||||
import { AiService } from './AiService.js';
|
import { AiService } from './AiService.js';
|
||||||
|
@ -202,6 +203,7 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx
|
||||||
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
||||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||||
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
||||||
|
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
|
||||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||||
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
||||||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
||||||
|
@ -348,6 +350,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
UserKeypairService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
UserMutingService,
|
UserMutingService,
|
||||||
|
UserSearchService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
UserAuthService,
|
UserAuthService,
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
|
@ -490,6 +493,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$UserKeypairService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
$UserMutingService,
|
$UserMutingService,
|
||||||
|
$UserSearchService,
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
$UserAuthService,
|
$UserAuthService,
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
|
@ -633,6 +637,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
UserKeypairService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
UserMutingService,
|
UserMutingService,
|
||||||
|
UserSearchService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
UserAuthService,
|
UserAuthService,
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
|
@ -774,6 +779,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$UserKeypairService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
$UserMutingService,
|
$UserMutingService,
|
||||||
|
$UserSearchService,
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
$UserAuthService,
|
$UserAuthService,
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
|
|
|
@ -40,6 +40,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
||||||
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
|
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
|
||||||
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
|
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
|
||||||
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
|
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
|
||||||
|
notRespondingSince: parsed.notRespondingSince ? new Date(parsed.notRespondingSince) : null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,6 +47,7 @@ export type RolePolicies = {
|
||||||
canHideAds: boolean;
|
canHideAds: boolean;
|
||||||
driveCapacityMb: number;
|
driveCapacityMb: number;
|
||||||
alwaysMarkNsfw: boolean;
|
alwaysMarkNsfw: boolean;
|
||||||
|
canUpdateBioMedia: boolean;
|
||||||
pinLimit: number;
|
pinLimit: number;
|
||||||
antennaLimit: number;
|
antennaLimit: number;
|
||||||
wordMuteLimit: number;
|
wordMuteLimit: number;
|
||||||
|
@ -75,6 +76,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
canHideAds: false,
|
canHideAds: false,
|
||||||
driveCapacityMb: 100,
|
driveCapacityMb: 100,
|
||||||
alwaysMarkNsfw: false,
|
alwaysMarkNsfw: false,
|
||||||
|
canUpdateBioMedia: true,
|
||||||
pinLimit: 5,
|
pinLimit: 5,
|
||||||
antennaLimit: 5,
|
antennaLimit: 5,
|
||||||
wordMuteLimit: 200,
|
wordMuteLimit: 200,
|
||||||
|
@ -376,6 +378,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||||
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
||||||
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
||||||
|
canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
|
||||||
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
|
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
|
||||||
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
|
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
|
||||||
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
|
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
|
||||||
|
|
205
packages/backend/src/core/UserSearchService.ts
Normal file
205
packages/backend/src/core/UserSearchService.ts
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
|
|
||||||
|
function defaultActiveThreshold() {
|
||||||
|
return new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserSearchService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザ名とホスト名によるユーザ検索を行う.
|
||||||
|
*
|
||||||
|
* - 検索結果には優先順位がつけられており、以下の順序で検索が行われる.
|
||||||
|
* 1. フォローしているユーザのうち、一定期間以内(※)に更新されたユーザ
|
||||||
|
* 2. フォローしているユーザのうち、一定期間以内に更新されていないユーザ
|
||||||
|
* 3. フォローしていないユーザのうち、一定期間以内に更新されたユーザ
|
||||||
|
* 4. フォローしていないユーザのうち、一定期間以内に更新されていないユーザ
|
||||||
|
* - ログインしていない場合は、以下の順序で検索が行われる.
|
||||||
|
* 1. 一定期間以内に更新されたユーザ
|
||||||
|
* 2. 一定期間以内に更新されていないユーザ
|
||||||
|
* - それぞれの検索結果はユーザ名の昇順でソートされる.
|
||||||
|
* - 動作的には先に登場した検索結果の登場位置が優先される(条件的にユーザIDが重複することはないが).
|
||||||
|
* (1で既にヒットしていた場合、2, 3, 4でヒットしても無視される)
|
||||||
|
* - ユーザ名とホスト名の検索条件はそれぞれ前方一致で検索される.
|
||||||
|
* - ユーザ名の検索は大文字小文字を区別しない.
|
||||||
|
* - ホスト名の検索は大文字小文字を区別しない.
|
||||||
|
* - 検索結果は最大で {@link opts.limit} 件までとなる.
|
||||||
|
*
|
||||||
|
* ※一定期間とは {@link params.activeThreshold} で指定された日時から現在までの期間を指す.
|
||||||
|
*
|
||||||
|
* @param params 検索条件.
|
||||||
|
* @param opts 関数の動作を制御するオプション.
|
||||||
|
* @param me 検索を実行するユーザの情報. 未ログインの場合は指定しない.
|
||||||
|
* @see {@link UserSearchService#buildSearchUserQueries}
|
||||||
|
* @see {@link UserSearchService#buildSearchUserNoLoginQueries}
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async search(
|
||||||
|
params: {
|
||||||
|
username?: string | null,
|
||||||
|
host?: string | null,
|
||||||
|
activeThreshold?: Date,
|
||||||
|
},
|
||||||
|
opts?: {
|
||||||
|
limit?: number,
|
||||||
|
detail?: boolean,
|
||||||
|
},
|
||||||
|
me?: MiUser | null,
|
||||||
|
): Promise<Packed<'User'>[]> {
|
||||||
|
const queries = me ? this.buildSearchUserQueries(me, params) : this.buildSearchUserNoLoginQueries(params);
|
||||||
|
|
||||||
|
let resultSet = new Set<MiUser['id']>();
|
||||||
|
const limit = opts?.limit ?? 10;
|
||||||
|
for (const query of queries) {
|
||||||
|
const ids = await query
|
||||||
|
.select('user.id')
|
||||||
|
.limit(limit - resultSet.size)
|
||||||
|
.orderBy('user.usernameLower', 'ASC')
|
||||||
|
.getRawMany<{ user_id: MiUser['id'] }>()
|
||||||
|
.then(res => res.map(x => x.user_id));
|
||||||
|
|
||||||
|
resultSet = new Set([...resultSet, ...ids]);
|
||||||
|
if (resultSet.size >= limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.userEntityService.packMany<'UserLite' | 'UserDetailed'>(
|
||||||
|
[...resultSet].slice(0, limit),
|
||||||
|
me,
|
||||||
|
{ schema: opts?.detail ? 'UserDetailed' : 'UserLite' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ログイン済みユーザによる検索実行時のクエリ一覧を構築する.
|
||||||
|
* @param me
|
||||||
|
* @param params
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
private buildSearchUserQueries(
|
||||||
|
me: MiUser,
|
||||||
|
params: {
|
||||||
|
username?: string | null,
|
||||||
|
host?: string | null,
|
||||||
|
activeThreshold?: Date,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
|
||||||
|
const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
|
||||||
|
|
||||||
|
const followingUserQuery = this.followingsRepository.createQueryBuilder('following')
|
||||||
|
.select('following.followeeId')
|
||||||
|
.where('following.followerId = :followerId', { followerId: me.id });
|
||||||
|
|
||||||
|
const activeFollowingUsersQuery = this.generateUserQueryBuilder(params)
|
||||||
|
.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
|
||||||
|
.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||||
|
activeFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
|
||||||
|
|
||||||
|
const inactiveFollowingUsersQuery = this.generateUserQueryBuilder(params)
|
||||||
|
.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
|
||||||
|
.andWhere(new Brackets(qb => {
|
||||||
|
qb
|
||||||
|
.where('user.updatedAt IS NULL')
|
||||||
|
.orWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
|
||||||
|
}));
|
||||||
|
inactiveFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
|
||||||
|
|
||||||
|
// 自分自身がヒットするとしたらここ
|
||||||
|
const activeUserQuery = this.generateUserQueryBuilder(params)
|
||||||
|
.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
|
||||||
|
.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||||
|
activeUserQuery.setParameters(followingUserQuery.getParameters());
|
||||||
|
|
||||||
|
const inactiveUserQuery = this.generateUserQueryBuilder(params)
|
||||||
|
.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
|
||||||
|
.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
|
||||||
|
inactiveUserQuery.setParameters(followingUserQuery.getParameters());
|
||||||
|
|
||||||
|
return [activeFollowingUsersQuery, inactiveFollowingUsersQuery, activeUserQuery, inactiveUserQuery];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ログインしていないユーザによる検索実行時のクエリ一覧を構築する.
|
||||||
|
* @param params
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
private buildSearchUserNoLoginQueries(params: {
|
||||||
|
username?: string | null,
|
||||||
|
host?: string | null,
|
||||||
|
activeThreshold?: Date,
|
||||||
|
}) {
|
||||||
|
// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
|
||||||
|
const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
|
||||||
|
|
||||||
|
const activeUserQuery = this.generateUserQueryBuilder(params)
|
||||||
|
.andWhere(new Brackets(qb => {
|
||||||
|
qb
|
||||||
|
.where('user.updatedAt IS NULL')
|
||||||
|
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||||
|
}));
|
||||||
|
|
||||||
|
const inactiveUserQuery = this.generateUserQueryBuilder(params)
|
||||||
|
.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
|
||||||
|
|
||||||
|
return [activeUserQuery, inactiveUserQuery];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザ検索クエリで共通する抽出条件をあらかじめ設定したクエリビルダを生成する.
|
||||||
|
* @param params
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
private generateUserQueryBuilder(params: {
|
||||||
|
username?: string | null,
|
||||||
|
host?: string | null,
|
||||||
|
}): SelectQueryBuilder<MiUser> {
|
||||||
|
const userQuery = this.usersRepository.createQueryBuilder('user');
|
||||||
|
|
||||||
|
if (params.username) {
|
||||||
|
userQuery.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(params.username.toLowerCase()) + '%' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.host) {
|
||||||
|
if (params.host === this.config.hostname || params.host === '.') {
|
||||||
|
userQuery.andWhere('user.host IS NULL');
|
||||||
|
} else {
|
||||||
|
userQuery.andWhere('user.host LIKE :host', {
|
||||||
|
host: sqlLikeEscape(params.host.toLowerCase()) + '%',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userQuery.andWhere('user.isSuspended = FALSE');
|
||||||
|
|
||||||
|
return userQuery;
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ export class ApMfmService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getNoteHtml(note: MiNote, apAppend?: string) {
|
public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, apAppend?: string) {
|
||||||
let noMisskeyContent = false;
|
let noMisskeyContent = false;
|
||||||
const srcMfm = (note.text ?? '') + (apAppend ?? '');
|
const srcMfm = (note.text ?? '') + (apAppend ?? '');
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { StatusError } from '@/misc/status-error.js';
|
||||||
import type { UtilityService } from '@/core/UtilityService.js';
|
import type { UtilityService } from '@/core/UtilityService.js';
|
||||||
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
|
@ -100,6 +101,8 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
private roleService: RoleService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,6 +241,11 @@ export class ApPersonService implements OnModuleInit {
|
||||||
return this.apImageService.resolveImage(user, img).catch(() => null);
|
return this.apImageService.resolveImage(user, img).catch(() => null);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (((avatar != null && avatar.id != null) || (banner != null && banner.id != null))
|
||||||
|
&& !(await this.roleService.getUserPolicies(user.id)).canUpdateBioMedia) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
we don't want to return nulls on errors! if the database fields
|
we don't want to return nulls on errors! if the database fields
|
||||||
are already null, nothing changes; if the database has old
|
are already null, nothing changes; if the database has old
|
||||||
|
|
|
@ -50,6 +50,22 @@ export class MetaEntityService {
|
||||||
}))
|
}))
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
|
// クライアントの手間を減らすためあらかじめJSONに変換しておく
|
||||||
|
let defaultLightTheme = null;
|
||||||
|
let defaultDarkTheme = null;
|
||||||
|
if (instance.defaultLightTheme) {
|
||||||
|
try {
|
||||||
|
defaultLightTheme = JSON.stringify(JSON5.parse(instance.defaultLightTheme));
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (instance.defaultDarkTheme) {
|
||||||
|
try {
|
||||||
|
defaultDarkTheme = JSON.stringify(JSON5.parse(instance.defaultDarkTheme));
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const packed: Packed<'MetaLite'> = {
|
const packed: Packed<'MetaLite'> = {
|
||||||
maintainerName: instance.maintainerName,
|
maintainerName: instance.maintainerName,
|
||||||
maintainerEmail: instance.maintainerEmail,
|
maintainerEmail: instance.maintainerEmail,
|
||||||
|
@ -90,9 +106,8 @@ export class MetaEntityService {
|
||||||
backgroundImageUrl: instance.backgroundImageUrl,
|
backgroundImageUrl: instance.backgroundImageUrl,
|
||||||
logoImageUrl: instance.logoImageUrl,
|
logoImageUrl: instance.logoImageUrl,
|
||||||
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
|
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
|
||||||
// クライアントの手間を減らすためあらかじめJSONに変換しておく
|
defaultLightTheme,
|
||||||
defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
|
defaultDarkTheme,
|
||||||
defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
|
|
||||||
ads: ads.map(ad => ({
|
ads: ads.map(ad => ({
|
||||||
id: ad.id,
|
id: ad.id,
|
||||||
url: ad.url,
|
url: ad.url,
|
||||||
|
|
|
@ -501,11 +501,15 @@ export class UserEntityService implements OnModuleInit {
|
||||||
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
||||||
onlineStatus: this.getOnlineStatus(user),
|
onlineStatus: this.getOnlineStatus(user),
|
||||||
// パフォーマンス上の理由でローカルユーザーのみ
|
// パフォーマンス上の理由でローカルユーザーのみ
|
||||||
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({
|
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
|
||||||
name: r.name,
|
.filter((r) => r.isPublic || iAmModerator)
|
||||||
iconUrl: r.iconUrl,
|
.sort((a, b) => b.displayOrder - a.displayOrder)
|
||||||
displayOrder: r.displayOrder,
|
.map((r) => ({
|
||||||
}))) : undefined,
|
name: r.name,
|
||||||
|
iconUrl: r.iconUrl,
|
||||||
|
displayOrder: r.displayOrder,
|
||||||
|
}))
|
||||||
|
) : undefined,
|
||||||
|
|
||||||
...(isDetailed ? {
|
...(isDetailed ? {
|
||||||
url: profile!.url,
|
url: profile!.url,
|
||||||
|
|
|
@ -228,6 +228,10 @@ export const packedRolePoliciesSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
canUpdateBioMedia: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
pinLimit: {
|
pinLimit: {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
|
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
|
||||||
import { HashtagService } from '@/core/HashtagService.js';
|
import { HashtagService } from '@/core/HashtagService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RolePolicies, RoleService } from '@/core/RoleService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
|
@ -256,6 +256,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
const profileUpdates = {} as Partial<MiUserProfile>;
|
const profileUpdates = {} as Partial<MiUserProfile>;
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||||
|
let policies: RolePolicies | null = null;
|
||||||
|
|
||||||
if (ps.name !== undefined) {
|
if (ps.name !== undefined) {
|
||||||
if (ps.name === null) {
|
if (ps.name === null) {
|
||||||
|
@ -296,14 +297,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.mutedWords !== undefined) {
|
if (ps.mutedWords !== undefined) {
|
||||||
checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||||
|
checkMuteWordCount(ps.mutedWords, policies.wordMuteLimit);
|
||||||
validateMuteWordRegex(ps.mutedWords);
|
validateMuteWordRegex(ps.mutedWords);
|
||||||
|
|
||||||
profileUpdates.mutedWords = ps.mutedWords;
|
profileUpdates.mutedWords = ps.mutedWords;
|
||||||
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
||||||
}
|
}
|
||||||
if (ps.hardMutedWords !== undefined) {
|
if (ps.hardMutedWords !== undefined) {
|
||||||
checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||||
|
checkMuteWordCount(ps.hardMutedWords, policies.wordMuteLimit);
|
||||||
validateMuteWordRegex(ps.hardMutedWords);
|
validateMuteWordRegex(ps.hardMutedWords);
|
||||||
profileUpdates.hardMutedWords = ps.hardMutedWords;
|
profileUpdates.hardMutedWords = ps.hardMutedWords;
|
||||||
}
|
}
|
||||||
|
@ -322,13 +325,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
||||||
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
||||||
if (typeof ps.alwaysMarkNsfw === 'boolean') {
|
if (typeof ps.alwaysMarkNsfw === 'boolean') {
|
||||||
if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
|
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||||
|
if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
|
||||||
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
|
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
|
||||||
}
|
}
|
||||||
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
|
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
|
||||||
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
|
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
|
||||||
|
|
||||||
if (ps.avatarId) {
|
if (ps.avatarId) {
|
||||||
|
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||||
|
if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
|
||||||
|
|
||||||
const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
|
const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
|
||||||
|
|
||||||
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
|
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
|
||||||
|
@ -344,6 +351,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.bannerId) {
|
if (ps.bannerId) {
|
||||||
|
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||||
|
if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
|
||||||
|
|
||||||
const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
|
const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
|
||||||
|
|
||||||
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
|
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
|
||||||
|
@ -359,14 +369,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.avatarDecorations) {
|
if (ps.avatarDecorations) {
|
||||||
|
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||||
const decorations = await this.avatarDecorationService.getAll(true);
|
const decorations = await this.avatarDecorationService.getAll(true);
|
||||||
const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]);
|
const myRoles = await this.roleService.getUserRoles(user.id);
|
||||||
const allRoles = await this.roleService.getRoles();
|
const allRoles = await this.roleService.getRoles();
|
||||||
const decorationIds = decorations
|
const decorationIds = decorations
|
||||||
.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
|
.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
|
||||||
.map(d => d.id);
|
.map(d => d.id);
|
||||||
|
|
||||||
if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
|
if (ps.avatarDecorations.length > policies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
|
||||||
|
|
||||||
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
|
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
|
||||||
id: d.id,
|
id: d.id,
|
||||||
|
|
|
@ -3,15 +3,9 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import type { UsersRepository, FollowingsRepository } from '@/models/_.js';
|
|
||||||
import type { Config } from '@/config.js';
|
|
||||||
import type { MiUser } from '@/models/User.js';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users'],
|
tags: ['users'],
|
||||||
|
@ -49,89 +43,16 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
private userSearchService: UserSearchService,
|
||||||
private config: Config,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
|
||||||
private followingsRepository: FollowingsRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, (ps, me) => {
|
||||||
const setUsernameAndHostQuery = (query = this.usersRepository.createQueryBuilder('user')) => {
|
return this.userSearchService.search({
|
||||||
if (ps.username) {
|
username: ps.username,
|
||||||
query.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
|
host: ps.host,
|
||||||
}
|
}, {
|
||||||
|
limit: ps.limit,
|
||||||
if (ps.host) {
|
detail: ps.detail,
|
||||||
if (ps.host === this.config.hostname || ps.host === '.') {
|
}, me);
|
||||||
query.andWhere('user.host IS NULL');
|
|
||||||
} else {
|
|
||||||
query.andWhere('user.host LIKE :host', {
|
|
||||||
host: sqlLikeEscape(ps.host.toLowerCase()) + '%',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
|
|
||||||
|
|
||||||
let users: MiUser[] = [];
|
|
||||||
|
|
||||||
if (me) {
|
|
||||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
|
||||||
.select('following.followeeId')
|
|
||||||
.where('following.followerId = :followerId', { followerId: me.id });
|
|
||||||
|
|
||||||
const query = setUsernameAndHostQuery()
|
|
||||||
.andWhere(`user.id IN (${ followingQuery.getQuery() })`)
|
|
||||||
.andWhere('user.id != :meId', { meId: me.id })
|
|
||||||
.andWhere('user.isSuspended = FALSE')
|
|
||||||
.andWhere(new Brackets(qb => {
|
|
||||||
qb
|
|
||||||
.where('user.updatedAt IS NULL')
|
|
||||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
|
||||||
}));
|
|
||||||
|
|
||||||
query.setParameters(followingQuery.getParameters());
|
|
||||||
|
|
||||||
users = await query
|
|
||||||
.orderBy('user.usernameLower', 'ASC')
|
|
||||||
.limit(ps.limit)
|
|
||||||
.getMany();
|
|
||||||
|
|
||||||
if (users.length < ps.limit) {
|
|
||||||
const otherQuery = setUsernameAndHostQuery()
|
|
||||||
.andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`)
|
|
||||||
.andWhere('user.isSuspended = FALSE')
|
|
||||||
.andWhere('user.updatedAt IS NOT NULL');
|
|
||||||
|
|
||||||
otherQuery.setParameters(followingQuery.getParameters());
|
|
||||||
|
|
||||||
const otherUsers = await otherQuery
|
|
||||||
.orderBy('user.updatedAt', 'DESC')
|
|
||||||
.limit(ps.limit - users.length)
|
|
||||||
.getMany();
|
|
||||||
|
|
||||||
users = users.concat(otherUsers);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const query = setUsernameAndHostQuery()
|
|
||||||
.andWhere('user.isSuspended = FALSE')
|
|
||||||
.andWhere('user.updatedAt IS NOT NULL');
|
|
||||||
|
|
||||||
users = await query
|
|
||||||
.orderBy('user.updatedAt', 'DESC')
|
|
||||||
.limit(ps.limit - users.length)
|
|
||||||
.getMany();
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export class OpenApiServerService {
|
||||||
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
|
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||||
fastify.get('/api-doc', async (_request, reply) => {
|
fastify.get('/api-doc', async (_request, reply) => {
|
||||||
reply.header('Cache-Control', 'public, max-age=86400');
|
reply.header('Cache-Control', 'public, max-age=86400');
|
||||||
return await reply.sendFile('/redoc.html', staticAssets);
|
return await reply.sendFile('/api-doc.html', staticAssets);
|
||||||
});
|
});
|
||||||
fastify.get('/api.json', (_request, reply) => {
|
fastify.get('/api.json', (_request, reply) => {
|
||||||
reply.header('Cache-Control', 'public, max-age=600');
|
reply.header('Cache-Control', 'public, max-age=600');
|
||||||
|
|
|
@ -15,7 +15,6 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
|
||||||
info: {
|
info: {
|
||||||
version: config.version,
|
version: config.version,
|
||||||
title: 'Misskey API',
|
title: 'Misskey API',
|
||||||
'x-logo': { url: '/static-assets/api-doc.png' },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
externalDocs: {
|
externalDocs: {
|
||||||
|
|
|
@ -206,7 +206,7 @@ describe('2要素認証', () => {
|
||||||
username,
|
username,
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(usersShowResponse.status, 200);
|
assert.strictEqual(usersShowResponse.status, 200);
|
||||||
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
|
assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
|
||||||
|
|
||||||
const signinResponse = await api('signin', {
|
const signinResponse = await api('signin', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
|
@ -248,7 +248,7 @@ describe('2要素認証', () => {
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
creationOptions: registerKeyResponse.body,
|
creationOptions: registerKeyResponse.body,
|
||||||
}) as any, alice);
|
} as any) as any, alice);
|
||||||
assert.strictEqual(keyDoneResponse.status, 200);
|
assert.strictEqual(keyDoneResponse.status, 200);
|
||||||
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
|
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
|
||||||
assert.strictEqual(keyDoneResponse.body.name, keyName);
|
assert.strictEqual(keyDoneResponse.body.name, keyName);
|
||||||
|
@ -257,22 +257,22 @@ describe('2要素認証', () => {
|
||||||
username,
|
username,
|
||||||
});
|
});
|
||||||
assert.strictEqual(usersShowResponse.status, 200);
|
assert.strictEqual(usersShowResponse.status, 200);
|
||||||
assert.strictEqual(usersShowResponse.body.securityKeys, true);
|
assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true);
|
||||||
|
|
||||||
const signinResponse = await api('signin', {
|
const signinResponse = await api('signin', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
});
|
});
|
||||||
assert.strictEqual(signinResponse.status, 200);
|
assert.strictEqual(signinResponse.status, 200);
|
||||||
assert.strictEqual(signinResponse.body.i, undefined);
|
assert.strictEqual(signinResponse.body.i, undefined);
|
||||||
assert.notEqual(signinResponse.body.challenge, undefined);
|
assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined);
|
||||||
assert.notEqual(signinResponse.body.allowCredentials, undefined);
|
assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined);
|
||||||
assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url'));
|
assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url'));
|
||||||
|
|
||||||
const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
|
const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
requestOptions: signinResponse.body,
|
requestOptions: signinResponse.body,
|
||||||
}));
|
} as any));
|
||||||
assert.strictEqual(signinResponse2.status, 200);
|
assert.strictEqual(signinResponse2.status, 200);
|
||||||
assert.notEqual(signinResponse2.body.i, undefined);
|
assert.notEqual(signinResponse2.body.i, undefined);
|
||||||
|
|
||||||
|
@ -307,7 +307,7 @@ describe('2要素認証', () => {
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
creationOptions: registerKeyResponse.body,
|
creationOptions: registerKeyResponse.body,
|
||||||
}) as any, alice);
|
} as any) as any, alice);
|
||||||
assert.strictEqual(keyDoneResponse.status, 200);
|
assert.strictEqual(keyDoneResponse.status, 200);
|
||||||
|
|
||||||
const passwordLessResponse = await api('i/2fa/password-less', {
|
const passwordLessResponse = await api('i/2fa/password-less', {
|
||||||
|
@ -319,7 +319,7 @@ describe('2要素認証', () => {
|
||||||
username,
|
username,
|
||||||
});
|
});
|
||||||
assert.strictEqual(usersShowResponse.status, 200);
|
assert.strictEqual(usersShowResponse.status, 200);
|
||||||
assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true);
|
assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true);
|
||||||
|
|
||||||
const signinResponse = await api('signin', {
|
const signinResponse = await api('signin', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
|
@ -333,7 +333,7 @@ describe('2要素認証', () => {
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
requestOptions: signinResponse.body,
|
requestOptions: signinResponse.body,
|
||||||
}),
|
} as any),
|
||||||
password: '',
|
password: '',
|
||||||
});
|
});
|
||||||
assert.strictEqual(signinResponse2.status, 200);
|
assert.strictEqual(signinResponse2.status, 200);
|
||||||
|
@ -370,7 +370,7 @@ describe('2要素認証', () => {
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
creationOptions: registerKeyResponse.body,
|
creationOptions: registerKeyResponse.body,
|
||||||
}) as any, alice);
|
} as any) as any, alice);
|
||||||
assert.strictEqual(keyDoneResponse.status, 200);
|
assert.strictEqual(keyDoneResponse.status, 200);
|
||||||
|
|
||||||
const renamedKey = 'other-key';
|
const renamedKey = 'other-key';
|
||||||
|
@ -383,6 +383,7 @@ describe('2要素認証', () => {
|
||||||
const iResponse = await api('i', {
|
const iResponse = await api('i', {
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(iResponse.status, 200);
|
assert.strictEqual(iResponse.status, 200);
|
||||||
|
assert.ok(iResponse.body.securityKeysList);
|
||||||
const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
|
const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
|
||||||
assert.strictEqual(securityKeys.length, 1);
|
assert.strictEqual(securityKeys.length, 1);
|
||||||
assert.strictEqual(securityKeys[0].name, renamedKey);
|
assert.strictEqual(securityKeys[0].name, renamedKey);
|
||||||
|
@ -419,13 +420,14 @@ describe('2要素認証', () => {
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
creationOptions: registerKeyResponse.body,
|
creationOptions: registerKeyResponse.body,
|
||||||
}) as any, alice);
|
} as any) as any, alice);
|
||||||
assert.strictEqual(keyDoneResponse.status, 200);
|
assert.strictEqual(keyDoneResponse.status, 200);
|
||||||
|
|
||||||
// テストの実行順によっては複数残ってるので全部消す
|
// テストの実行順によっては複数残ってるので全部消す
|
||||||
const iResponse = await api('i', {
|
const iResponse = await api('i', {
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(iResponse.status, 200);
|
assert.strictEqual(iResponse.status, 200);
|
||||||
|
assert.ok(iResponse.body.securityKeysList);
|
||||||
for (const key of iResponse.body.securityKeysList) {
|
for (const key of iResponse.body.securityKeysList) {
|
||||||
const removeKeyResponse = await api('i/2fa/remove-key', {
|
const removeKeyResponse = await api('i/2fa/remove-key', {
|
||||||
token: otpToken(registerResponse.body.secret),
|
token: otpToken(registerResponse.body.secret),
|
||||||
|
@ -439,7 +441,7 @@ describe('2要素認証', () => {
|
||||||
username,
|
username,
|
||||||
});
|
});
|
||||||
assert.strictEqual(usersShowResponse.status, 200);
|
assert.strictEqual(usersShowResponse.status, 200);
|
||||||
assert.strictEqual(usersShowResponse.body.securityKeys, false);
|
assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false);
|
||||||
|
|
||||||
const signinResponse = await api('signin', {
|
const signinResponse = await api('signin', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
|
@ -470,7 +472,7 @@ describe('2要素認証', () => {
|
||||||
username,
|
username,
|
||||||
});
|
});
|
||||||
assert.strictEqual(usersShowResponse.status, 200);
|
assert.strictEqual(usersShowResponse.status, 200);
|
||||||
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
|
assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
|
||||||
|
|
||||||
const unregisterResponse = await api('i/2fa/unregister', {
|
const unregisterResponse = await api('i/2fa/unregister', {
|
||||||
token: otpToken(registerResponse.body.secret),
|
token: otpToken(registerResponse.body.secret),
|
||||||
|
|
|
@ -410,21 +410,21 @@ describe('API visibility', () => {
|
||||||
test('[HTL] public-post が 自分が見れる', async () => {
|
test('[HTL] public-post が 自分が見れる', async () => {
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
const notes = res.body.filter((n: any) => n.id === pub.id);
|
const notes = res.body.filter(n => n.id === pub.id);
|
||||||
assert.strictEqual(notes[0].text, 'x');
|
assert.strictEqual(notes[0].text, 'x');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[HTL] public-post が 非フォロワーから見れない', async () => {
|
test('[HTL] public-post が 非フォロワーから見れない', async () => {
|
||||||
const res = await api('notes/timeline', { limit: 100 }, other);
|
const res = await api('notes/timeline', { limit: 100 }, other);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
const notes = res.body.filter((n: any) => n.id === pub.id);
|
const notes = res.body.filter(n => n.id === pub.id);
|
||||||
assert.strictEqual(notes.length, 0);
|
assert.strictEqual(notes.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[HTL] followers-post が フォロワーから見れる', async () => {
|
test('[HTL] followers-post が フォロワーから見れる', async () => {
|
||||||
const res = await api('notes/timeline', { limit: 100 }, follower);
|
const res = await api('notes/timeline', { limit: 100 }, follower);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
const notes = res.body.filter((n: any) => n.id === fol.id);
|
const notes = res.body.filter(n => n.id === fol.id);
|
||||||
assert.strictEqual(notes[0].text, 'x');
|
assert.strictEqual(notes[0].text, 'x');
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -433,21 +433,21 @@ describe('API visibility', () => {
|
||||||
test('[replies] followers-reply が フォロワーから見れる', async () => {
|
test('[replies] followers-reply が フォロワーから見れる', async () => {
|
||||||
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower);
|
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
const notes = res.body.filter((n: any) => n.id === folR.id);
|
const notes = res.body.filter(n => n.id === folR.id);
|
||||||
assert.strictEqual(notes[0].text, 'x');
|
assert.strictEqual(notes[0].text, 'x');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
|
test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
|
||||||
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other);
|
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
const notes = res.body.filter((n: any) => n.id === folR.id);
|
const notes = res.body.filter(n => n.id === folR.id);
|
||||||
assert.strictEqual(notes.length, 0);
|
assert.strictEqual(notes.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
|
test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
|
||||||
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target);
|
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
const notes = res.body.filter((n: any) => n.id === folR.id);
|
const notes = res.body.filter(n => n.id === folR.id);
|
||||||
assert.strictEqual(notes[0].text, 'x');
|
assert.strictEqual(notes[0].text, 'x');
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -456,14 +456,14 @@ describe('API visibility', () => {
|
||||||
test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
|
test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
|
||||||
const res = await api('notes/mentions', { limit: 100 }, target);
|
const res = await api('notes/mentions', { limit: 100 }, target);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
const notes = res.body.filter((n: any) => n.id === folR.id);
|
const notes = res.body.filter(n => n.id === folR.id);
|
||||||
assert.strictEqual(notes[0].text, 'x');
|
assert.strictEqual(notes[0].text, 'x');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
|
test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
|
||||||
const res = await api('notes/mentions', { limit: 100 }, target);
|
const res = await api('notes/mentions', { limit: 100 }, target);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
const notes = res.body.filter((n: any) => n.id === folM.id);
|
const notes = res.body.filter(n => n.id === folM.id);
|
||||||
assert.strictEqual(notes[0].text, '@target x');
|
assert.strictEqual(notes[0].text, '@target x');
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { api, post, signup } from '../utils.js';
|
import { api, castAsError, post, signup } from '../utils.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Block', () => {
|
describe('Block', () => {
|
||||||
|
@ -33,7 +33,7 @@ describe('Block', () => {
|
||||||
const res = await api('following/create', { userId: alice.id }, bob);
|
const res = await api('following/create', { userId: alice.id }, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
|
assert.strictEqual(castAsError(res.body).error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ブロックされているユーザーにリアクションできない', async () => {
|
test('ブロックされているユーザーにリアクションできない', async () => {
|
||||||
|
@ -42,7 +42,8 @@ describe('Block', () => {
|
||||||
const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
|
const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
|
assert.ok(res.body);
|
||||||
|
assert.strictEqual(castAsError(res.body).error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ブロックされているユーザーに返信できない', async () => {
|
test('ブロックされているユーザーに返信できない', async () => {
|
||||||
|
@ -51,7 +52,8 @@ describe('Block', () => {
|
||||||
const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob);
|
const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
|
assert.ok(res.body);
|
||||||
|
assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ブロックされているユーザーのノートをRenoteできない', async () => {
|
test('ブロックされているユーザーのノートをRenoteできない', async () => {
|
||||||
|
@ -60,7 +62,7 @@ describe('Block', () => {
|
||||||
const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob);
|
const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
|
assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: ユーザーリストに入れられないテスト
|
// TODO: ユーザーリストに入れられないテスト
|
||||||
|
|
|
@ -79,14 +79,14 @@ describe('クリップ', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => {
|
const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => {
|
||||||
return await successfulApiCall({
|
await successfulApiCall({
|
||||||
endpoint: 'clips/delete',
|
endpoint: 'clips/delete',
|
||||||
parameters,
|
parameters,
|
||||||
user: alice,
|
user: alice,
|
||||||
...request,
|
...request,
|
||||||
}, {
|
}, {
|
||||||
status: 204,
|
status: 204,
|
||||||
}) as any as void;
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial<ApiRequest<'clips/show'>> = {}): Promise<Misskey.entities.Clip> => {
|
const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial<ApiRequest<'clips/show'>> = {}): Promise<Misskey.entities.Clip> => {
|
||||||
|
@ -454,25 +454,25 @@ describe('クリップ', () => {
|
||||||
let aliceClip: Misskey.entities.Clip;
|
let aliceClip: Misskey.entities.Clip;
|
||||||
|
|
||||||
const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => {
|
const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => {
|
||||||
return successfulApiCall({
|
await successfulApiCall({
|
||||||
endpoint: 'clips/favorite',
|
endpoint: 'clips/favorite',
|
||||||
parameters,
|
parameters,
|
||||||
user: alice,
|
user: alice,
|
||||||
...request,
|
...request,
|
||||||
}, {
|
}, {
|
||||||
status: 204,
|
status: 204,
|
||||||
}) as any as void;
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => {
|
const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => {
|
||||||
return successfulApiCall({
|
await successfulApiCall({
|
||||||
endpoint: 'clips/unfavorite',
|
endpoint: 'clips/unfavorite',
|
||||||
parameters,
|
parameters,
|
||||||
user: alice,
|
user: alice,
|
||||||
...request,
|
...request,
|
||||||
}, {
|
}, {
|
||||||
status: 204,
|
status: 204,
|
||||||
}) as any as void;
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => {
|
const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import * as assert from 'assert';
|
||||||
// https://github.com/node-fetch/node-fetch/pull/1664
|
// https://github.com/node-fetch/node-fetch/pull/1664
|
||||||
import { Blob } from 'node-fetch';
|
import { Blob } from 'node-fetch';
|
||||||
import { MiUser } from '@/models/_.js';
|
import { MiUser } from '@/models/_.js';
|
||||||
import { api, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
|
import { api, castAsError, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Endpoints', () => {
|
describe('Endpoints', () => {
|
||||||
|
@ -164,7 +164,7 @@ describe('Endpoints', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.id, alice.id);
|
assert.strictEqual((res.body as unknown as { id: string }).id, alice.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ユーザーが存在しなかったら怒る', async () => {
|
test('ユーザーが存在しなかったら怒る', async () => {
|
||||||
|
@ -285,7 +285,8 @@ describe('Endpoints', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'CANNOT_REACT_TO_RENOTE');
|
assert.ok(res.body);
|
||||||
|
assert.strictEqual(castAsError(res.body).error.code, 'CANNOT_REACT_TO_RENOTE');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('引用にリアクションできる', async () => {
|
test('引用にリアクションできる', async () => {
|
||||||
|
@ -1063,7 +1064,7 @@ describe('Endpoints', () => {
|
||||||
userId: bob.id,
|
userId: bob.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res1.status, 204);
|
assert.strictEqual(res1.status, 204);
|
||||||
assert.strictEqual(res2.body?.memo, memo);
|
assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('自分に関するメモを更新できる', async () => {
|
test('自分に関するメモを更新できる', async () => {
|
||||||
|
@ -1078,7 +1079,7 @@ describe('Endpoints', () => {
|
||||||
userId: alice.id,
|
userId: alice.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res1.status, 204);
|
assert.strictEqual(res1.status, 204);
|
||||||
assert.strictEqual(res2.body?.memo, memo);
|
assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('メモを削除できる', async () => {
|
test('メモを削除できる', async () => {
|
||||||
|
@ -1099,7 +1100,7 @@ describe('Endpoints', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
// memoには常に文字列かnullが入っている(5cac151)
|
// memoには常に文字列かnullが入っている(5cac151)
|
||||||
assert.strictEqual(res.body.memo, null);
|
assert.strictEqual((res.body as unknown as { memo: string | null }).memo, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('メモは個人ごとに独立して保存される', async () => {
|
test('メモは個人ごとに独立して保存される', async () => {
|
||||||
|
@ -1126,8 +1127,8 @@ describe('Endpoints', () => {
|
||||||
}, carol),
|
}, carol),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.strictEqual(resAlice.body.memo, memoAliceToBob);
|
assert.strictEqual((resAlice.body as unknown as { memo: string }).memo, memoAliceToBob);
|
||||||
assert.strictEqual(resCarol.body.memo, memoCarolToBob);
|
assert.strictEqual((resCarol.body as unknown as { memo: string }).memo, memoCarolToBob);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -61,14 +61,14 @@ describe('export-clips', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('basic export', async () => {
|
test('basic export', async () => {
|
||||||
let res = await api('clips/create', {
|
const res1 = await api('clips/create', {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
description: 'bar',
|
description: 'bar',
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res1.status, 200);
|
||||||
|
|
||||||
res = await api('i/export-clips', {}, alice);
|
const res2 = await api('i/export-clips', {}, alice);
|
||||||
assert.strictEqual(res.status, 204);
|
assert.strictEqual(res2.status, 204);
|
||||||
|
|
||||||
const exported = await pollFirstDriveFile();
|
const exported = await pollFirstDriveFile();
|
||||||
assert.strictEqual(exported[0].name, 'foo');
|
assert.strictEqual(exported[0].name, 'foo');
|
||||||
|
@ -77,7 +77,7 @@ describe('export-clips', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('export with notes', async () => {
|
test('export with notes', async () => {
|
||||||
let res = await api('clips/create', {
|
const res = await api('clips/create', {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
description: 'bar',
|
description: 'bar',
|
||||||
}, alice);
|
}, alice);
|
||||||
|
@ -96,15 +96,15 @@ describe('export-clips', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const note of [note1, note2]) {
|
for (const note of [note1, note2]) {
|
||||||
res = await api('clips/add-note', {
|
const res2 = await api('clips/add-note', {
|
||||||
clipId: clip.id,
|
clipId: clip.id,
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 204);
|
assert.strictEqual(res2.status, 204);
|
||||||
}
|
}
|
||||||
|
|
||||||
res = await api('i/export-clips', {}, alice);
|
const res3 = await api('i/export-clips', {}, alice);
|
||||||
assert.strictEqual(res.status, 204);
|
assert.strictEqual(res3.status, 204);
|
||||||
|
|
||||||
const exported = await pollFirstDriveFile();
|
const exported = await pollFirstDriveFile();
|
||||||
assert.strictEqual(exported[0].name, 'foo');
|
assert.strictEqual(exported[0].name, 'foo');
|
||||||
|
@ -116,19 +116,19 @@ describe('export-clips', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('multiple clips', async () => {
|
test('multiple clips', async () => {
|
||||||
let res = await api('clips/create', {
|
const res1 = await api('clips/create', {
|
||||||
name: 'kawaii',
|
name: 'kawaii',
|
||||||
description: 'kawaii',
|
description: 'kawaii',
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res1.status, 200);
|
||||||
const clip1 = res.body;
|
const clip1 = res1.body;
|
||||||
|
|
||||||
res = await api('clips/create', {
|
const res2 = await api('clips/create', {
|
||||||
name: 'yuri',
|
name: 'yuri',
|
||||||
description: 'yuri',
|
description: 'yuri',
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res2.status, 200);
|
||||||
const clip2 = res.body;
|
const clip2 = res2.body;
|
||||||
|
|
||||||
const note1 = await post(alice, {
|
const note1 = await post(alice, {
|
||||||
text: 'baz1',
|
text: 'baz1',
|
||||||
|
@ -138,20 +138,26 @@ describe('export-clips', () => {
|
||||||
text: 'baz2',
|
text: 'baz2',
|
||||||
});
|
});
|
||||||
|
|
||||||
res = await api('clips/add-note', {
|
{
|
||||||
clipId: clip1.id,
|
const res = await api('clips/add-note', {
|
||||||
noteId: note1.id,
|
clipId: clip1.id,
|
||||||
}, alice);
|
noteId: note1.id,
|
||||||
assert.strictEqual(res.status, 204);
|
}, alice);
|
||||||
|
assert.strictEqual(res.status, 204);
|
||||||
|
}
|
||||||
|
|
||||||
res = await api('clips/add-note', {
|
{
|
||||||
clipId: clip2.id,
|
const res = await api('clips/add-note', {
|
||||||
noteId: note2.id,
|
clipId: clip2.id,
|
||||||
}, alice);
|
noteId: note2.id,
|
||||||
assert.strictEqual(res.status, 204);
|
}, alice);
|
||||||
|
assert.strictEqual(res.status, 204);
|
||||||
|
}
|
||||||
|
|
||||||
res = await api('i/export-clips', {}, alice);
|
{
|
||||||
assert.strictEqual(res.status, 204);
|
const res = await api('i/export-clips', {}, alice);
|
||||||
|
assert.strictEqual(res.status, 204);
|
||||||
|
}
|
||||||
|
|
||||||
const exported = await pollFirstDriveFile();
|
const exported = await pollFirstDriveFile();
|
||||||
assert.strictEqual(exported[0].name, 'kawaii');
|
assert.strictEqual(exported[0].name, 'kawaii');
|
||||||
|
@ -163,7 +169,7 @@ describe('export-clips', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Clipping other user\'s note', async () => {
|
test('Clipping other user\'s note', async () => {
|
||||||
let res = await api('clips/create', {
|
const res = await api('clips/create', {
|
||||||
name: 'kawaii',
|
name: 'kawaii',
|
||||||
description: 'kawaii',
|
description: 'kawaii',
|
||||||
}, alice);
|
}, alice);
|
||||||
|
@ -175,14 +181,14 @@ describe('export-clips', () => {
|
||||||
visibility: 'followers',
|
visibility: 'followers',
|
||||||
});
|
});
|
||||||
|
|
||||||
res = await api('clips/add-note', {
|
const res2 = await api('clips/add-note', {
|
||||||
clipId: clip.id,
|
clipId: clip.id,
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 204);
|
assert.strictEqual(res2.status, 204);
|
||||||
|
|
||||||
res = await api('i/export-clips', {}, alice);
|
const res3 = await api('i/export-clips', {}, alice);
|
||||||
assert.strictEqual(res.status, 204);
|
assert.strictEqual(res3.status, 204);
|
||||||
|
|
||||||
const exported = await pollFirstDriveFile();
|
const exported = await pollFirstDriveFile();
|
||||||
assert.strictEqual(exported[0].name, 'kawaii');
|
assert.strictEqual(exported[0].name, 'kawaii');
|
||||||
|
|
|
@ -13,14 +13,14 @@ import { loadConfig } from '@/config.js';
|
||||||
import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js';
|
import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { jobQueue } from '@/boot/common.js';
|
import { jobQueue } from '@/boot/common.js';
|
||||||
import { api, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js';
|
import { api, castAsError, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Account Move', () => {
|
describe('Account Move', () => {
|
||||||
let jq: INestApplicationContext;
|
let jq: INestApplicationContext;
|
||||||
let url: URL;
|
let url: URL;
|
||||||
|
|
||||||
let root: any;
|
let root: misskey.entities.SignupResponse;
|
||||||
let alice: misskey.entities.SignupResponse;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.SignupResponse;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.SignupResponse;
|
let carol: misskey.entities.SignupResponse;
|
||||||
|
@ -93,8 +93,8 @@ describe('Account Move', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_USER');
|
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER');
|
||||||
assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Unable to add duplicated aliases to alsoKnownAs', async () => {
|
test('Unable to add duplicated aliases to alsoKnownAs', async () => {
|
||||||
|
@ -103,8 +103,8 @@ describe('Account Move', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'INVALID_PARAM');
|
assert.strictEqual(castAsError(res.body).error.code, 'INVALID_PARAM');
|
||||||
assert.strictEqual(res.body.error.id, '3d81ceae-475f-4600-b2a8-2bc116157532');
|
assert.strictEqual(castAsError(res.body).error.id, '3d81ceae-475f-4600-b2a8-2bc116157532');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Unable to add itself', async () => {
|
test('Unable to add itself', async () => {
|
||||||
|
@ -113,8 +113,8 @@ describe('Account Move', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'FORBIDDEN_TO_SET_YOURSELF');
|
assert.strictEqual(castAsError(res.body).error.code, 'FORBIDDEN_TO_SET_YOURSELF');
|
||||||
assert.strictEqual(res.body.error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4');
|
assert.strictEqual(castAsError(res.body).error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
|
test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
|
||||||
|
@ -123,16 +123,16 @@ describe('Account Move', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res1.status, 400);
|
assert.strictEqual(res1.status, 400);
|
||||||
assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER');
|
assert.strictEqual(castAsError(res1.body).error.code, 'NO_SUCH_USER');
|
||||||
assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
assert.strictEqual(castAsError(res1.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||||
|
|
||||||
const res2 = await api('i/update', {
|
const res2 = await api('i/update', {
|
||||||
alsoKnownAs: ['@alice', 'nonexist'],
|
alsoKnownAs: ['@alice', 'nonexist'],
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res2.status, 400);
|
assert.strictEqual(res2.status, 400);
|
||||||
assert.strictEqual(res2.body.error.code, 'NO_SUCH_USER');
|
assert.strictEqual(castAsError(res2.body).error.code, 'NO_SUCH_USER');
|
||||||
assert.strictEqual(res2.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
assert.strictEqual(castAsError(res2.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Able to add two existing local account to alsoKnownAs', async () => {
|
test('Able to add two existing local account to alsoKnownAs', async () => {
|
||||||
|
@ -241,8 +241,8 @@ describe('Account Move', () => {
|
||||||
}, root);
|
}, root);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'NOT_ROOT_FORBIDDEN');
|
assert.strictEqual(castAsError(res.body).error.code, 'NOT_ROOT_FORBIDDEN');
|
||||||
assert.strictEqual(res.body.error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24');
|
assert.strictEqual(castAsError(res.body).error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Unable to move to a nonexisting local account', async () => {
|
test('Unable to move to a nonexisting local account', async () => {
|
||||||
|
@ -251,8 +251,8 @@ describe('Account Move', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_USER');
|
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER');
|
||||||
assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Unable to move if alsoKnownAs is invalid', async () => {
|
test('Unable to move if alsoKnownAs is invalid', async () => {
|
||||||
|
@ -261,8 +261,8 @@ describe('Account Move', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
||||||
assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Relationships have been properly migrated', async () => {
|
test('Relationships have been properly migrated', async () => {
|
||||||
|
@ -279,36 +279,44 @@ describe('Account Move', () => {
|
||||||
userId: alice.id,
|
userId: alice.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(aliceFollowings.status, 200);
|
assert.strictEqual(aliceFollowings.status, 200);
|
||||||
|
assert.ok(aliceFollowings);
|
||||||
assert.strictEqual(aliceFollowings.body.length, 3);
|
assert.strictEqual(aliceFollowings.body.length, 3);
|
||||||
|
|
||||||
const carolFollowings = await api('users/following', {
|
const carolFollowings = await api('users/following', {
|
||||||
userId: carol.id,
|
userId: carol.id,
|
||||||
}, carol);
|
}, carol);
|
||||||
assert.strictEqual(carolFollowings.status, 200);
|
assert.strictEqual(carolFollowings.status, 200);
|
||||||
|
assert.ok(carolFollowings);
|
||||||
assert.strictEqual(carolFollowings.body.length, 2);
|
assert.strictEqual(carolFollowings.body.length, 2);
|
||||||
assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
|
assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
|
||||||
assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
|
assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
|
||||||
|
|
||||||
const blockings = await api('blocking/list', {}, dave);
|
const blockings = await api('blocking/list', {}, dave);
|
||||||
assert.strictEqual(blockings.status, 200);
|
assert.strictEqual(blockings.status, 200);
|
||||||
|
assert.ok(blockings);
|
||||||
assert.strictEqual(blockings.body.length, 2);
|
assert.strictEqual(blockings.body.length, 2);
|
||||||
assert.strictEqual(blockings.body[0].blockeeId, bob.id);
|
assert.strictEqual(blockings.body[0].blockeeId, bob.id);
|
||||||
assert.strictEqual(blockings.body[1].blockeeId, alice.id);
|
assert.strictEqual(blockings.body[1].blockeeId, alice.id);
|
||||||
|
|
||||||
const mutings = await api('mute/list', {}, dave);
|
const mutings = await api('mute/list', {}, dave);
|
||||||
assert.strictEqual(mutings.status, 200);
|
assert.strictEqual(mutings.status, 200);
|
||||||
|
assert.ok(mutings);
|
||||||
assert.strictEqual(mutings.body.length, 2);
|
assert.strictEqual(mutings.body.length, 2);
|
||||||
assert.strictEqual(mutings.body[0].muteeId, bob.id);
|
assert.strictEqual(mutings.body[0].muteeId, bob.id);
|
||||||
assert.strictEqual(mutings.body[1].muteeId, alice.id);
|
assert.strictEqual(mutings.body[1].muteeId, alice.id);
|
||||||
|
|
||||||
const rootLists = await api('users/lists/list', {}, root);
|
const rootLists = await api('users/lists/list', {}, root);
|
||||||
assert.strictEqual(rootLists.status, 200);
|
assert.strictEqual(rootLists.status, 200);
|
||||||
|
assert.ok(rootLists);
|
||||||
|
assert.ok(rootLists.body[0].userIds);
|
||||||
assert.strictEqual(rootLists.body[0].userIds.length, 2);
|
assert.strictEqual(rootLists.body[0].userIds.length, 2);
|
||||||
assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id));
|
assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id));
|
||||||
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
|
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
|
||||||
|
|
||||||
const eveLists = await api('users/lists/list', {}, eve);
|
const eveLists = await api('users/lists/list', {}, eve);
|
||||||
assert.strictEqual(eveLists.status, 200);
|
assert.strictEqual(eveLists.status, 200);
|
||||||
|
assert.ok(eveLists);
|
||||||
|
assert.ok(eveLists.body[0].userIds);
|
||||||
assert.strictEqual(eveLists.body[0].userIds.length, 1);
|
assert.strictEqual(eveLists.body[0].userIds.length, 1);
|
||||||
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
|
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
|
||||||
});
|
});
|
||||||
|
@ -347,8 +355,8 @@ describe('Account Move', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
||||||
assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Follow and follower counts are properly adjusted', async () => {
|
test('Follow and follower counts are properly adjusted', async () => {
|
||||||
|
@ -419,8 +427,9 @@ describe('Account Move', () => {
|
||||||
] as const)('Prohibit access after moving: %s', async (endpoint) => {
|
] as const)('Prohibit access after moving: %s', async (endpoint) => {
|
||||||
const res = await api(endpoint, {}, alice);
|
const res = await api(endpoint, {}, alice);
|
||||||
assert.strictEqual(res.status, 403);
|
assert.strictEqual(res.status, 403);
|
||||||
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
|
assert.ok(res.body);
|
||||||
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
|
||||||
|
assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Prohibit access after moving: /antennas/update', async () => {
|
test('Prohibit access after moving: /antennas/update', async () => {
|
||||||
|
@ -438,16 +447,19 @@ describe('Account Move', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 403);
|
assert.strictEqual(res.status, 403);
|
||||||
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
|
assert.ok(res.body);
|
||||||
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
|
||||||
|
assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Prohibit access after moving: /drive/files/create', async () => {
|
test('Prohibit access after moving: /drive/files/create', async () => {
|
||||||
|
// FIXME: 一旦逃げておく
|
||||||
const res = await uploadFile(alice);
|
const res = await uploadFile(alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 403);
|
assert.strictEqual(res.status, 403);
|
||||||
assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.code, 'YOUR_ACCOUNT_MOVED');
|
assert.ok(res.body);
|
||||||
assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
|
||||||
|
assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Prohibit updating alsoKnownAs after moving', async () => {
|
test('Prohibit updating alsoKnownAs after moving', async () => {
|
||||||
|
@ -456,8 +468,8 @@ describe('Account Move', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 403);
|
assert.strictEqual(res.status, 403);
|
||||||
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
|
assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
|
||||||
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,8 +47,8 @@ describe('Mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
|
test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
|
||||||
|
@ -92,9 +92,9 @@ describe('Mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
|
test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
|
||||||
|
@ -108,9 +108,9 @@ describe('Mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -124,8 +124,8 @@ describe('Mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||||
|
@ -138,8 +138,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||||
|
@ -152,8 +152,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
|
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
|
||||||
|
@ -166,8 +166,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
|
test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
|
||||||
|
@ -180,8 +180,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
|
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
|
||||||
|
@ -193,8 +193,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
|
|
||||||
await api('following/delete', { userId: alice.id }, bob);
|
await api('following/delete', { userId: alice.id }, bob);
|
||||||
await api('following/delete', { userId: alice.id }, carol);
|
await api('following/delete', { userId: alice.id }, carol);
|
||||||
|
@ -210,8 +210,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
|
|
||||||
await api('following/delete', { userId: alice.id }, bob);
|
await api('following/delete', { userId: alice.id }, bob);
|
||||||
await api('following/delete', { userId: alice.id }, carol);
|
await api('following/delete', { userId: alice.id }, carol);
|
||||||
|
@ -228,8 +228,8 @@ describe('Mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
});
|
});
|
||||||
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||||
const aliceNote = await post(alice, { text: 'hi' });
|
const aliceNote = await post(alice, { text: 'hi' });
|
||||||
|
@ -241,8 +241,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||||
|
@ -255,8 +255,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
|
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
|
||||||
|
@ -269,8 +269,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
|
test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
|
||||||
|
@ -283,8 +283,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
|
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
|
||||||
|
@ -296,8 +296,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
|
|
||||||
await api('following/delete', { userId: alice.id }, bob);
|
await api('following/delete', { userId: alice.id }, bob);
|
||||||
await api('following/delete', { userId: alice.id }, carol);
|
await api('following/delete', { userId: alice.id }, carol);
|
||||||
|
@ -313,8 +313,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,16 +3,18 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Repository } from "typeorm";
|
||||||
|
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||||
import { api, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js';
|
import { api, castAsError, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Note', () => {
|
describe('Note', () => {
|
||||||
let Notes: any;
|
let Notes: Repository<MiNote>;
|
||||||
|
|
||||||
let root: misskey.entities.SignupResponse;
|
let root: misskey.entities.SignupResponse;
|
||||||
let alice: misskey.entities.SignupResponse;
|
let alice: misskey.entities.SignupResponse;
|
||||||
|
@ -61,8 +63,8 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
|
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
|
||||||
assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
||||||
}, 1000 * 10);
|
}, 1000 * 10);
|
||||||
|
|
||||||
test('存在しないファイルで怒られる', async () => {
|
test('存在しないファイルで怒られる', async () => {
|
||||||
|
@ -72,8 +74,8 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
|
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
|
||||||
assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('不正なファイルIDで怒られる', async () => {
|
test('不正なファイルIDで怒られる', async () => {
|
||||||
|
@ -81,8 +83,8 @@ describe('Note', () => {
|
||||||
fileIds: ['kyoppie'],
|
fileIds: ['kyoppie'],
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
|
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
|
||||||
assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('返信できる', async () => {
|
test('返信できる', async () => {
|
||||||
|
@ -101,6 +103,7 @@ describe('Note', () => {
|
||||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.createdNote.text, alicePost.text);
|
assert.strictEqual(res.body.createdNote.text, alicePost.text);
|
||||||
assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
|
assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
|
||||||
|
assert.ok(res.body.createdNote.reply);
|
||||||
assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
|
assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -118,6 +121,7 @@ describe('Note', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
|
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
|
||||||
|
assert.ok(res.body.createdNote.renote);
|
||||||
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
|
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -137,6 +141,7 @@ describe('Note', () => {
|
||||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.createdNote.text, alicePost.text);
|
assert.strictEqual(res.body.createdNote.text, alicePost.text);
|
||||||
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
|
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
|
||||||
|
assert.ok(res.body.createdNote.renote);
|
||||||
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
|
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -218,7 +223,7 @@ describe('Note', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(bobReply.status, 400);
|
assert.strictEqual(bobReply.status, 400);
|
||||||
assert.strictEqual(bobReply.body.error.code, 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE');
|
assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('visibility: specifiedなノートに対してvisibility: specifiedで返信できる', async () => {
|
test('visibility: specifiedなノートに対してvisibility: specifiedで返信できる', async () => {
|
||||||
|
@ -256,7 +261,7 @@ describe('Note', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(bobReply.status, 400);
|
assert.strictEqual(bobReply.status, 400);
|
||||||
assert.strictEqual(bobReply.body.error.code, 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY');
|
assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('文字数ぎりぎりで怒られない', async () => {
|
test('文字数ぎりぎりで怒られない', async () => {
|
||||||
|
@ -333,6 +338,7 @@ describe('Note', () => {
|
||||||
assert.strictEqual(res.body.createdNote.text, post.text);
|
assert.strictEqual(res.body.createdNote.text, post.text);
|
||||||
|
|
||||||
const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id });
|
const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id });
|
||||||
|
assert.ok(noteDoc);
|
||||||
assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
|
assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -345,6 +351,7 @@ describe('Note', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||||
|
assert.ok(res.body.createdNote.files);
|
||||||
assert.strictEqual(res.body.createdNote.files.length, 1);
|
assert.strictEqual(res.body.createdNote.files.length, 1);
|
||||||
assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id);
|
assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id);
|
||||||
});
|
});
|
||||||
|
@ -363,8 +370,9 @@ describe('Note', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id);
|
const myNote = res.body.find(note => note.id === createdNote.body.createdNote.id);
|
||||||
assert.notEqual(myNote, null);
|
assert.ok(myNote);
|
||||||
|
assert.ok(myNote.files);
|
||||||
assert.strictEqual(myNote.files.length, 1);
|
assert.strictEqual(myNote.files.length, 1);
|
||||||
assert.strictEqual(myNote.files[0].id, file.body!.id);
|
assert.strictEqual(myNote.files[0].id, file.body!.id);
|
||||||
});
|
});
|
||||||
|
@ -389,7 +397,9 @@ describe('Note', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
|
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
|
||||||
assert.notEqual(myNote, null);
|
assert.ok(myNote);
|
||||||
|
assert.ok(myNote.renote);
|
||||||
|
assert.ok(myNote.renote.files);
|
||||||
assert.strictEqual(myNote.renote.files.length, 1);
|
assert.strictEqual(myNote.renote.files.length, 1);
|
||||||
assert.strictEqual(myNote.renote.files[0].id, file.body!.id);
|
assert.strictEqual(myNote.renote.files[0].id, file.body!.id);
|
||||||
});
|
});
|
||||||
|
@ -415,7 +425,9 @@ describe('Note', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id);
|
const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id);
|
||||||
assert.notEqual(myNote, null);
|
assert.ok(myNote);
|
||||||
|
assert.ok(myNote.reply);
|
||||||
|
assert.ok(myNote.reply.files);
|
||||||
assert.strictEqual(myNote.reply.files.length, 1);
|
assert.strictEqual(myNote.reply.files.length, 1);
|
||||||
assert.strictEqual(myNote.reply.files[0].id, file.body!.id);
|
assert.strictEqual(myNote.reply.files[0].id, file.body!.id);
|
||||||
});
|
});
|
||||||
|
@ -446,7 +458,10 @@ describe('Note', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
|
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
|
||||||
assert.notEqual(myNote, null);
|
assert.ok(myNote);
|
||||||
|
assert.ok(myNote.renote);
|
||||||
|
assert.ok(myNote.renote.reply);
|
||||||
|
assert.ok(myNote.renote.reply.files);
|
||||||
assert.strictEqual(myNote.renote.reply.files.length, 1);
|
assert.strictEqual(myNote.renote.reply.files.length, 1);
|
||||||
assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id);
|
assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id);
|
||||||
});
|
});
|
||||||
|
@ -474,7 +489,7 @@ describe('Note', () => {
|
||||||
priority: 0,
|
priority: 0,
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
} as any,
|
},
|
||||||
}, root);
|
}, root);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
@ -498,7 +513,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(liftnsfw.status, 400);
|
assert.strictEqual(liftnsfw.status, 400);
|
||||||
assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE');
|
assert.strictEqual(castAsError(liftnsfw.body).error.code, 'RESTRICTED_BY_ROLE');
|
||||||
|
|
||||||
const oldaddnsfw = await api('drive/files/update', {
|
const oldaddnsfw = await api('drive/files/update', {
|
||||||
fileId: file.body!.id,
|
fileId: file.body!.id,
|
||||||
|
@ -710,7 +725,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(note1.status, 400);
|
assert.strictEqual(note1.status, 400);
|
||||||
assert.strictEqual(note1.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
|
assert.strictEqual(castAsError(note1.body).error.code, 'CONTAINS_PROHIBITED_WORDS');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('禁止ワードを含む投稿はエラーになる (正規表現)', async () => {
|
test('禁止ワードを含む投稿はエラーになる (正規表現)', async () => {
|
||||||
|
@ -727,7 +742,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(note2.status, 400);
|
assert.strictEqual(note2.status, 400);
|
||||||
assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
|
assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('禁止ワードを含む投稿はエラーになる (スペースアンド)', async () => {
|
test('禁止ワードを含む投稿はエラーになる (スペースアンド)', async () => {
|
||||||
|
@ -744,7 +759,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(note2.status, 400);
|
assert.strictEqual(note2.status, 400);
|
||||||
assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
|
assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('禁止ワードを含んでるリモートノートもエラーになる', async () => {
|
test('禁止ワードを含んでるリモートノートもエラーになる', async () => {
|
||||||
|
@ -786,7 +801,7 @@ describe('Note', () => {
|
||||||
priority: 1,
|
priority: 1,
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
} as any,
|
},
|
||||||
}, root);
|
}, root);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
@ -807,7 +822,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(note.status, 400);
|
assert.strictEqual(note.status, 400);
|
||||||
assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS');
|
assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS');
|
||||||
|
|
||||||
await api('admin/roles/unassign', {
|
await api('admin/roles/unassign', {
|
||||||
userId: alice.id,
|
userId: alice.id,
|
||||||
|
@ -840,7 +855,7 @@ describe('Note', () => {
|
||||||
priority: 1,
|
priority: 1,
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
} as any,
|
},
|
||||||
}, root);
|
}, root);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
@ -863,7 +878,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(note.status, 400);
|
assert.strictEqual(note.status, 400);
|
||||||
assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS');
|
assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS');
|
||||||
|
|
||||||
await api('admin/roles/unassign', {
|
await api('admin/roles/unassign', {
|
||||||
userId: alice.id,
|
userId: alice.id,
|
||||||
|
@ -896,7 +911,7 @@ describe('Note', () => {
|
||||||
priority: 1,
|
priority: 1,
|
||||||
value: 1,
|
value: 1,
|
||||||
},
|
},
|
||||||
} as any,
|
},
|
||||||
}, root);
|
}, root);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
@ -951,6 +966,7 @@ describe('Note', () => {
|
||||||
|
|
||||||
assert.strictEqual(deleteOneRes.status, 204);
|
assert.strictEqual(deleteOneRes.status, 204);
|
||||||
let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
|
let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
|
||||||
|
assert.ok(mainNote);
|
||||||
assert.strictEqual(mainNote.repliesCount, 1);
|
assert.strictEqual(mainNote.repliesCount, 1);
|
||||||
|
|
||||||
const deleteTwoRes = await api('notes/delete', {
|
const deleteTwoRes = await api('notes/delete', {
|
||||||
|
@ -959,6 +975,7 @@ describe('Note', () => {
|
||||||
|
|
||||||
assert.strictEqual(deleteTwoRes.status, 204);
|
assert.strictEqual(deleteTwoRes.status, 204);
|
||||||
mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
|
mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
|
||||||
|
assert.ok(mainNote);
|
||||||
assert.strictEqual(mainNote.repliesCount, 0);
|
assert.strictEqual(mainNote.repliesCount, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -980,7 +997,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'UNAVAILABLE');
|
assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -992,7 +1009,7 @@ describe('Note', () => {
|
||||||
const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice);
|
const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_NOTE');
|
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_NOTE');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('不可視なノートは翻訳できない', async () => {
|
test('不可視なノートは翻訳できない', async () => {
|
||||||
|
@ -1000,7 +1017,7 @@ describe('Note', () => {
|
||||||
const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob);
|
const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob);
|
||||||
|
|
||||||
assert.strictEqual(bobTranslateAttempt.status, 400);
|
assert.strictEqual(bobTranslateAttempt.status, 400);
|
||||||
assert.strictEqual(bobTranslateAttempt.body.error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE');
|
assert.strictEqual(castAsError(bobTranslateAttempt.body).error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('text: null なノートを翻訳すると空のレスポンスが返ってくる', async () => {
|
test('text: null なノートを翻訳すると空のレスポンスが返ってくる', async () => {
|
||||||
|
@ -1016,7 +1033,7 @@ describe('Note', () => {
|
||||||
|
|
||||||
// NOTE: デフォルトでは登録されていないので落ちる
|
// NOTE: デフォルトでは登録されていないので落ちる
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'UNAVAILABLE');
|
assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -42,9 +42,9 @@ describe('Renote Mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolRenote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('タイムラインにリノートミュートしているユーザーの引用が含まれる', async () => {
|
test('タイムラインにリノートミュートしているユーザーの引用が含まれる', async () => {
|
||||||
|
@ -59,9 +59,9 @@ describe('Renote Mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === carolRenote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// #12956
|
// #12956
|
||||||
|
@ -76,8 +76,8 @@ describe('Renote Mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobRenote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => {
|
test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => {
|
||||||
|
|
|
@ -33,9 +33,9 @@ describe('Note thread mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolReply.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolReplyWithoutMention.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
|
test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
|
||||||
|
@ -93,8 +93,8 @@ describe('Note thread mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false);
|
assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReply.id), false);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false);
|
assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReplyWithoutMention.id), false);
|
||||||
|
|
||||||
// NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
|
// NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
|
||||||
});
|
});
|
||||||
|
|
|
@ -37,8 +37,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||||
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
|
assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているユーザーのノートが含まれる', async () => {
|
test.concurrent('フォローしているユーザーのノートが含まれる', async () => {
|
||||||
|
@ -53,8 +53,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
|
test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
|
||||||
|
@ -69,9 +69,9 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => {
|
test.concurrent('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => {
|
||||||
|
@ -86,8 +86,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => {
|
test.concurrent('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => {
|
||||||
|
@ -103,8 +103,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => {
|
test.concurrent('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => {
|
||||||
|
@ -120,8 +120,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
|
test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
|
||||||
|
@ -137,8 +137,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => {
|
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => {
|
||||||
|
@ -156,9 +156,9 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||||
assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id).text, 'hi');
|
assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => {
|
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => {
|
||||||
|
@ -175,8 +175,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => {
|
test.concurrent('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => {
|
||||||
|
@ -191,8 +191,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
|
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
|
||||||
|
@ -207,8 +207,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('自分の他人への返信が含まれる', async () => {
|
test.concurrent('自分の他人への返信が含まれる', async () => {
|
||||||
|
@ -221,8 +221,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => {
|
test.concurrent('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => {
|
||||||
|
@ -237,8 +237,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => {
|
test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => {
|
||||||
|
@ -255,8 +255,8 @@ describe('Timelines', () => {
|
||||||
withRenotes: false,
|
withRenotes: false,
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => {
|
test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => {
|
||||||
|
@ -273,8 +273,8 @@ describe('Timelines', () => {
|
||||||
withRenotes: false,
|
withRenotes: false,
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => {
|
test.concurrent('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => {
|
||||||
|
@ -288,7 +288,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
|
test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
|
||||||
|
@ -304,8 +304,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
|
test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
|
||||||
|
@ -322,8 +322,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
||||||
|
@ -338,7 +338,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
||||||
|
@ -353,7 +353,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => {
|
test.concurrent('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => {
|
||||||
|
@ -374,10 +374,10 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice);
|
const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote1.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote2.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false);
|
||||||
}, 1000 * 10);
|
}, 1000 * 10);
|
||||||
|
|
||||||
test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
|
test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
|
||||||
|
@ -392,7 +392,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('自分の visibility: specified なノートが含まれる', async () => {
|
test.concurrent('自分の visibility: specified なノートが含まれる', async () => {
|
||||||
|
@ -404,8 +404,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||||
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
|
assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => {
|
test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => {
|
||||||
|
@ -419,8 +419,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => {
|
test.concurrent('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => {
|
||||||
|
@ -432,7 +432,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => {
|
test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => {
|
||||||
|
@ -446,7 +446,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしていないユーザーからの visibility: specified なノートに返信したときの自身のノートが含まれる', async () => {
|
test.concurrent('フォローしていないユーザーからの visibility: specified なノートに返信したときの自身のノートが含まれる', async () => {
|
||||||
|
@ -459,8 +459,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||||
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'ok');
|
assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'ok');
|
||||||
});
|
});
|
||||||
|
|
||||||
/* TODO
|
/* TODO
|
||||||
|
@ -474,8 +474,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'ok');
|
assert.strictEqual(res.body.find(note => note.id === bobNote.id).text, 'ok');
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -490,7 +490,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -505,8 +505,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('他人の他人への返信が含まれない', async () => {
|
test.concurrent('他人の他人への返信が含まれない', async () => {
|
||||||
|
@ -519,8 +519,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('他人のその人自身への返信が含まれる', async () => {
|
test.concurrent('他人のその人自身への返信が含まれる', async () => {
|
||||||
|
@ -533,8 +533,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('チャンネル投稿が含まれない', async () => {
|
test.concurrent('チャンネル投稿が含まれない', async () => {
|
||||||
|
@ -547,7 +547,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('リモートユーザーのノートが含まれない', async () => {
|
test.concurrent('リモートユーザーのノートが含まれない', async () => {
|
||||||
|
@ -559,7 +559,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 含まれても良いと思うけど実装が面倒なので含まれない
|
// 含まれても良いと思うけど実装が面倒なので含まれない
|
||||||
|
@ -575,8 +575,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('ミュートしているユーザーのノートが含まれない', async () => {
|
test.concurrent('ミュートしているユーザーのノートが含まれない', async () => {
|
||||||
|
@ -591,8 +591,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
|
test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
|
||||||
|
@ -608,8 +608,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
|
test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
|
||||||
|
@ -626,8 +626,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
|
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
|
||||||
|
@ -642,8 +642,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => {
|
test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => {
|
||||||
|
@ -656,7 +656,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice);
|
const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
|
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
|
||||||
|
@ -670,8 +670,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice);
|
const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||||
}, 1000 * 10);
|
}, 1000 * 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -685,7 +685,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('ローカルユーザーの visibility: home なノートが含まれない', async () => {
|
test.concurrent('ローカルユーザーの visibility: home なノートが含まれない', async () => {
|
||||||
|
@ -697,7 +697,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => {
|
test.concurrent('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => {
|
||||||
|
@ -711,7 +711,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
|
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
|
||||||
|
@ -726,8 +726,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('他人の他人への返信が含まれない', async () => {
|
test.concurrent('他人の他人への返信が含まれない', async () => {
|
||||||
|
@ -740,8 +740,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('リモートユーザーのノートが含まれない', async () => {
|
test.concurrent('リモートユーザーのノートが含まれない', async () => {
|
||||||
|
@ -753,7 +753,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
||||||
|
@ -768,7 +768,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
||||||
|
@ -783,7 +783,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => {
|
test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => {
|
||||||
|
@ -796,7 +796,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice);
|
const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
|
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
|
||||||
|
@ -810,8 +810,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice);
|
const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||||
}, 1000 * 10);
|
}, 1000 * 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -828,7 +828,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => {
|
test.concurrent('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => {
|
||||||
|
@ -843,7 +843,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
|
test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
|
||||||
|
@ -858,7 +858,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
|
test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
|
||||||
|
@ -874,7 +874,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => {
|
test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => {
|
||||||
|
@ -890,8 +890,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => {
|
test.concurrent('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => {
|
||||||
|
@ -908,7 +908,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
|
test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
|
||||||
|
@ -925,7 +925,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => {
|
test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => {
|
||||||
|
@ -942,7 +942,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => {
|
test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => {
|
||||||
|
@ -958,7 +958,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => {
|
test.concurrent('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => {
|
||||||
|
@ -974,8 +974,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('リスインしている自分の visibility: followers なノートが含まれる', async () => {
|
test.concurrent('リスインしている自分の visibility: followers なノートが含まれる', async () => {
|
||||||
|
@ -990,8 +990,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||||
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
|
assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => {
|
test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => {
|
||||||
|
@ -1007,7 +1007,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => {
|
test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => {
|
||||||
|
@ -1023,8 +1023,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||||
}, 1000 * 10);
|
}, 1000 * 10);
|
||||||
|
|
||||||
test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => {
|
test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => {
|
||||||
|
@ -1039,8 +1039,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => {
|
test.concurrent('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => {
|
||||||
|
@ -1056,7 +1056,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1070,7 +1070,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
|
test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
|
||||||
|
@ -1082,7 +1082,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
|
test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
|
||||||
|
@ -1096,8 +1096,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('自身の visibility: followers なノートが含まれる', async () => {
|
test.concurrent('自身の visibility: followers なノートが含まれる', async () => {
|
||||||
|
@ -1109,8 +1109,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: alice.id }, alice);
|
const res = await api('users/notes', { userId: alice.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||||
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
|
assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('チャンネル投稿が含まれない', async () => {
|
test.concurrent('チャンネル投稿が含まれない', async () => {
|
||||||
|
@ -1123,7 +1123,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => {
|
test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => {
|
||||||
|
@ -1137,8 +1137,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => {
|
test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => {
|
||||||
|
@ -1152,8 +1152,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
|
const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => {
|
test.concurrent('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => {
|
||||||
|
@ -1167,8 +1167,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
|
const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
|
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
|
||||||
|
@ -1182,8 +1182,8 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice);
|
const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||||
}, 1000 * 10);
|
}, 1000 * 10);
|
||||||
|
|
||||||
test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => {
|
test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => {
|
||||||
|
@ -1196,7 +1196,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
|
const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => {
|
test.concurrent('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => {
|
||||||
|
@ -1209,7 +1209,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
|
const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('[withChannelNotes: true] 自分が取得した場合センシティブチャンネル投稿が含まれる', async () => {
|
test.concurrent('[withChannelNotes: true] 自分が取得した場合センシティブチャンネル投稿が含まれる', async () => {
|
||||||
|
@ -1222,7 +1222,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob);
|
const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => {
|
test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => {
|
||||||
|
@ -1237,7 +1237,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
|
test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
|
||||||
|
@ -1253,9 +1253,9 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote3.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('自身の visibility: specified なノートが含まれる', async () => {
|
test.concurrent('自身の visibility: specified なノートが含まれる', async () => {
|
||||||
|
@ -1267,7 +1267,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice);
|
const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => {
|
test.concurrent('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => {
|
||||||
|
@ -1279,7 +1279,7 @@ describe('Timelines', () => {
|
||||||
|
|
||||||
const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
|
const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** @see https://github.com/misskey-dev/misskey/issues/14000 */
|
/** @see https://github.com/misskey-dev/misskey/issues/14000 */
|
||||||
|
|
|
@ -231,7 +231,7 @@ describe('ユーザー', () => {
|
||||||
rolePublic = await role(root, { isPublic: true, name: 'Public Role' });
|
rolePublic = await role(root, { isPublic: true, name: 'Public Role' });
|
||||||
await api('admin/roles/assign', { userId: userRolePublic.id, roleId: rolePublic.id }, root);
|
await api('admin/roles/assign', { userId: userRolePublic.id, roleId: rolePublic.id }, root);
|
||||||
userRoleBadge = await signup({ username: 'userRoleBadge' });
|
userRoleBadge = await signup({ username: 'userRoleBadge' });
|
||||||
roleBadge = await role(root, { asBadge: true, name: 'Badge Role' });
|
roleBadge = await role(root, { asBadge: true, name: 'Badge Role', isPublic: true });
|
||||||
await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root);
|
await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root);
|
||||||
userSilenced = await signup({ username: 'userSilenced' });
|
userSilenced = await signup({ username: 'userSilenced' });
|
||||||
await post(userSilenced, { text: 'test' });
|
await post(userSilenced, { text: 'test' });
|
||||||
|
@ -655,7 +655,16 @@ describe('ユーザー', () => {
|
||||||
iconUrl: roleBadge.iconUrl,
|
iconUrl: roleBadge.iconUrl,
|
||||||
displayOrder: roleBadge.displayOrder,
|
displayOrder: roleBadge.displayOrder,
|
||||||
}]);
|
}]);
|
||||||
assert.deepStrictEqual(response.roles, []); // バッヂだからといってrolesが取れるとは限らない
|
assert.deepStrictEqual(response.roles, [{
|
||||||
|
id: roleBadge.id,
|
||||||
|
name: roleBadge.name,
|
||||||
|
color: roleBadge.color,
|
||||||
|
iconUrl: roleBadge.iconUrl,
|
||||||
|
description: roleBadge.description,
|
||||||
|
isModerator: roleBadge.isModerator,
|
||||||
|
isAdministrator: roleBadge.isAdministrator,
|
||||||
|
displayOrder: roleBadge.displayOrder,
|
||||||
|
}]);
|
||||||
});
|
});
|
||||||
test('をID指定のリスト形式で取得することができる(空)', async () => {
|
test('をID指定のリスト形式で取得することができる(空)', async () => {
|
||||||
const parameters = { userIds: [] };
|
const parameters = { userIds: [] };
|
||||||
|
|
|
@ -23,10 +23,10 @@ describe('ApMfmService', () => {
|
||||||
|
|
||||||
describe('getNoteHtml', () => {
|
describe('getNoteHtml', () => {
|
||||||
test('Do not provide _misskey_content for simple text', () => {
|
test('Do not provide _misskey_content for simple text', () => {
|
||||||
const note: MiNote = {
|
const note = {
|
||||||
text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com',
|
text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com',
|
||||||
mentionedRemoteUsers: '[]',
|
mentionedRemoteUsers: '[]',
|
||||||
} as any;
|
};
|
||||||
|
|
||||||
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||||
|
|
||||||
|
@ -35,10 +35,10 @@ describe('ApMfmService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Provide _misskey_content for MFM', () => {
|
test('Provide _misskey_content for MFM', () => {
|
||||||
const note: MiNote = {
|
const note = {
|
||||||
text: '$[tada foo]',
|
text: '$[tada foo]',
|
||||||
mentionedRemoteUsers: '[]',
|
mentionedRemoteUsers: '[]',
|
||||||
} as any;
|
};
|
||||||
|
|
||||||
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { ModuleMocker } from 'jest-mock';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { afterAll, beforeAll, describe, test } from '@jest/globals';
|
import { afterAll, beforeAll, describe, test } from '@jest/globals';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { FileInfoService } from '@/core/FileInfoService.js';
|
import { FileInfo, FileInfoService } from '@/core/FileInfoService.js';
|
||||||
//import { DI } from '@/di-symbols.js';
|
//import { DI } from '@/di-symbols.js';
|
||||||
import { AiService } from '@/core/AiService.js';
|
import { AiService } from '@/core/AiService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
@ -28,6 +28,15 @@ const moduleMocker = new ModuleMocker(global);
|
||||||
describe('FileInfoService', () => {
|
describe('FileInfoService', () => {
|
||||||
let app: TestingModule;
|
let app: TestingModule;
|
||||||
let fileInfoService: FileInfoService;
|
let fileInfoService: FileInfoService;
|
||||||
|
const strip = (fileInfo: FileInfo): Omit<Partial<FileInfo>, 'warnings' | 'blurhash' | 'sensitive' | 'porn'> => {
|
||||||
|
const fi: Partial<FileInfo> = fileInfo;
|
||||||
|
delete fi.warnings;
|
||||||
|
delete fi.sensitive;
|
||||||
|
delete fi.blurhash;
|
||||||
|
delete fi.porn;
|
||||||
|
|
||||||
|
return fi;
|
||||||
|
}
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await Test.createTestingModule({
|
app = await Test.createTestingModule({
|
||||||
|
@ -63,11 +72,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('Empty file', async () => {
|
test('Empty file', async () => {
|
||||||
const path = `${resources}/emptyfile`;
|
const path = `${resources}/emptyfile`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 0,
|
size: 0,
|
||||||
md5: 'd41d8cd98f00b204e9800998ecf8427e',
|
md5: 'd41d8cd98f00b204e9800998ecf8427e',
|
||||||
|
@ -84,11 +89,7 @@ describe('FileInfoService', () => {
|
||||||
describe('IMAGE', () => {
|
describe('IMAGE', () => {
|
||||||
test('Generic JPEG', async () => {
|
test('Generic JPEG', async () => {
|
||||||
const path = `${resources}/192.jpg`;
|
const path = `${resources}/192.jpg`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 5131,
|
size: 5131,
|
||||||
md5: '8c9ed0677dd2b8f9f7472c3af247e5e3',
|
md5: '8c9ed0677dd2b8f9f7472c3af247e5e3',
|
||||||
|
@ -104,11 +105,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('Generic APNG', async () => {
|
test('Generic APNG', async () => {
|
||||||
const path = `${resources}/anime.png`;
|
const path = `${resources}/anime.png`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 1868,
|
size: 1868,
|
||||||
md5: '08189c607bea3b952704676bb3c979e0',
|
md5: '08189c607bea3b952704676bb3c979e0',
|
||||||
|
@ -124,11 +121,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('Generic AGIF', async () => {
|
test('Generic AGIF', async () => {
|
||||||
const path = `${resources}/anime.gif`;
|
const path = `${resources}/anime.gif`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 2248,
|
size: 2248,
|
||||||
md5: '32c47a11555675d9267aee1a86571e7e',
|
md5: '32c47a11555675d9267aee1a86571e7e',
|
||||||
|
@ -144,11 +137,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('PNG with alpha', async () => {
|
test('PNG with alpha', async () => {
|
||||||
const path = `${resources}/with-alpha.png`;
|
const path = `${resources}/with-alpha.png`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 3772,
|
size: 3772,
|
||||||
md5: 'f73535c3e1e27508885b69b10cf6e991',
|
md5: 'f73535c3e1e27508885b69b10cf6e991',
|
||||||
|
@ -164,11 +153,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('Generic SVG', async () => {
|
test('Generic SVG', async () => {
|
||||||
const path = `${resources}/image.svg`;
|
const path = `${resources}/image.svg`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 505,
|
size: 505,
|
||||||
md5: 'b6f52b4b021e7b92cdd04509c7267965',
|
md5: 'b6f52b4b021e7b92cdd04509c7267965',
|
||||||
|
@ -185,11 +170,7 @@ describe('FileInfoService', () => {
|
||||||
test('SVG with XML definition', async () => {
|
test('SVG with XML definition', async () => {
|
||||||
// https://github.com/misskey-dev/misskey/issues/4413
|
// https://github.com/misskey-dev/misskey/issues/4413
|
||||||
const path = `${resources}/with-xml-def.svg`;
|
const path = `${resources}/with-xml-def.svg`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 544,
|
size: 544,
|
||||||
md5: '4b7a346cde9ccbeb267e812567e33397',
|
md5: '4b7a346cde9ccbeb267e812567e33397',
|
||||||
|
@ -205,11 +186,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('Dimension limit', async () => {
|
test('Dimension limit', async () => {
|
||||||
const path = `${resources}/25000x25000.png`;
|
const path = `${resources}/25000x25000.png`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 75933,
|
size: 75933,
|
||||||
md5: '268c5dde99e17cf8fe09f1ab3f97df56',
|
md5: '268c5dde99e17cf8fe09f1ab3f97df56',
|
||||||
|
@ -225,11 +202,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('Rotate JPEG', async () => {
|
test('Rotate JPEG', async () => {
|
||||||
const path = `${resources}/rotate.jpg`;
|
const path = `${resources}/rotate.jpg`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 12624,
|
size: 12624,
|
||||||
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
|
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
|
||||||
|
@ -247,11 +220,7 @@ describe('FileInfoService', () => {
|
||||||
describe('AUDIO', () => {
|
describe('AUDIO', () => {
|
||||||
test('MP3', async () => {
|
test('MP3', async () => {
|
||||||
const path = `${resources}/kick_gaba7.mp3`;
|
const path = `${resources}/kick_gaba7.mp3`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
delete info.width;
|
delete info.width;
|
||||||
delete info.height;
|
delete info.height;
|
||||||
delete info.orientation;
|
delete info.orientation;
|
||||||
|
@ -267,11 +236,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('WAV', async () => {
|
test('WAV', async () => {
|
||||||
const path = `${resources}/kick_gaba7.wav`;
|
const path = `${resources}/kick_gaba7.wav`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
delete info.width;
|
delete info.width;
|
||||||
delete info.height;
|
delete info.height;
|
||||||
delete info.orientation;
|
delete info.orientation;
|
||||||
|
@ -287,11 +252,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('AAC', async () => {
|
test('AAC', async () => {
|
||||||
const path = `${resources}/kick_gaba7.aac`;
|
const path = `${resources}/kick_gaba7.aac`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
delete info.width;
|
delete info.width;
|
||||||
delete info.height;
|
delete info.height;
|
||||||
delete info.orientation;
|
delete info.orientation;
|
||||||
|
@ -307,11 +268,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('FLAC', async () => {
|
test('FLAC', async () => {
|
||||||
const path = `${resources}/kick_gaba7.flac`;
|
const path = `${resources}/kick_gaba7.flac`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
delete info.width;
|
delete info.width;
|
||||||
delete info.height;
|
delete info.height;
|
||||||
delete info.orientation;
|
delete info.orientation;
|
||||||
|
@ -327,11 +284,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('MPEG-4 AUDIO (M4A)', async () => {
|
test('MPEG-4 AUDIO (M4A)', async () => {
|
||||||
const path = `${resources}/kick_gaba7.m4a`;
|
const path = `${resources}/kick_gaba7.m4a`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
delete info.width;
|
delete info.width;
|
||||||
delete info.height;
|
delete info.height;
|
||||||
delete info.orientation;
|
delete info.orientation;
|
||||||
|
@ -347,11 +300,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('WEBM AUDIO', async () => {
|
test('WEBM AUDIO', async () => {
|
||||||
const path = `${resources}/kick_gaba7.webm`;
|
const path = `${resources}/kick_gaba7.webm`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
delete info.width;
|
delete info.width;
|
||||||
delete info.height;
|
delete info.height;
|
||||||
delete info.orientation;
|
delete info.orientation;
|
||||||
|
|
265
packages/backend/test/unit/UserSearchService.ts
Normal file
265
packages/backend/test/unit/UserSearchService.ts
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { describe, jest, test } from '@jest/globals';
|
||||||
|
import { In } from 'typeorm';
|
||||||
|
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||||
|
import { FollowingsRepository, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
|
||||||
|
describe('UserSearchService', () => {
|
||||||
|
let app: TestingModule;
|
||||||
|
let service: UserSearchService;
|
||||||
|
|
||||||
|
let usersRepository: UsersRepository;
|
||||||
|
let followingsRepository: FollowingsRepository;
|
||||||
|
let idService: IdService;
|
||||||
|
let userProfilesRepository: UserProfilesRepository;
|
||||||
|
|
||||||
|
let root: MiUser;
|
||||||
|
let alice: MiUser;
|
||||||
|
let alyce: MiUser;
|
||||||
|
let alycia: MiUser;
|
||||||
|
let alysha: MiUser;
|
||||||
|
let alyson: MiUser;
|
||||||
|
let alyssa: MiUser;
|
||||||
|
let bob: MiUser;
|
||||||
|
let bobbi: MiUser;
|
||||||
|
let bobbie: MiUser;
|
||||||
|
let bobby: MiUser;
|
||||||
|
|
||||||
|
async function createUser(data: Partial<MiUser> = {}) {
|
||||||
|
const user = await usersRepository
|
||||||
|
.insert({
|
||||||
|
id: idService.gen(),
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
|
await userProfilesRepository.insert({
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createFollowings(follower: MiUser, followees: MiUser[]) {
|
||||||
|
for (const followee of followees) {
|
||||||
|
await followingsRepository.insert({
|
||||||
|
id: idService.gen(),
|
||||||
|
followerId: follower.id,
|
||||||
|
followeeId: followee.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setActive(users: MiUser[]) {
|
||||||
|
for (const user of users) {
|
||||||
|
await usersRepository.update(user.id, {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setInactive(users: MiUser[]) {
|
||||||
|
for (const user of users) {
|
||||||
|
await usersRepository.update(user.id, {
|
||||||
|
updatedAt: new Date(0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setSuspended(users: MiUser[]) {
|
||||||
|
for (const user of users) {
|
||||||
|
await usersRepository.update(user.id, {
|
||||||
|
isSuspended: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await Test
|
||||||
|
.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
GlobalModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
UserSearchService,
|
||||||
|
{
|
||||||
|
provide: UserEntityService, useFactory: jest.fn(() => ({
|
||||||
|
// とりあえずIDが返れば確認が出来るので
|
||||||
|
packMany: (value: any) => value,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
IdService,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
|
||||||
|
usersRepository = app.get(DI.usersRepository);
|
||||||
|
userProfilesRepository = app.get(DI.userProfilesRepository);
|
||||||
|
followingsRepository = app.get(DI.followingsRepository);
|
||||||
|
|
||||||
|
service = app.get(UserSearchService);
|
||||||
|
idService = app.get(IdService);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
|
||||||
|
alice = await createUser({ username: 'Alice', usernameLower: 'alice' });
|
||||||
|
alyce = await createUser({ username: 'Alyce', usernameLower: 'alyce' });
|
||||||
|
alycia = await createUser({ username: 'Alycia', usernameLower: 'alycia' });
|
||||||
|
alysha = await createUser({ username: 'Alysha', usernameLower: 'alysha' });
|
||||||
|
alyson = await createUser({ username: 'Alyson', usernameLower: 'alyson', host: 'example.com' });
|
||||||
|
alyssa = await createUser({ username: 'Alyssa', usernameLower: 'alyssa', host: 'example.com' });
|
||||||
|
bob = await createUser({ username: 'Bob', usernameLower: 'bob' });
|
||||||
|
bobbi = await createUser({ username: 'Bobbi', usernameLower: 'bobbi' });
|
||||||
|
bobbie = await createUser({ username: 'Bobbie', usernameLower: 'bobbie', host: 'example.com' });
|
||||||
|
bobby = await createUser({ username: 'Bobby', usernameLower: 'bobby', host: 'example.com' });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await usersRepository.delete({});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('search', () => {
|
||||||
|
test('フォロー中のアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||||
|
await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||||
|
await setActive([alice, alyce, alyssa, bob, bobbi, bobbie, bobby]);
|
||||||
|
await setInactive([alycia, alysha, alyson]);
|
||||||
|
|
||||||
|
const result = await service.search(
|
||||||
|
{ username: 'al' },
|
||||||
|
{ limit: 100 },
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
// alycia, alysha, alysonは非アクティブなので後ろに行く
|
||||||
|
expect(result).toEqual([alice, alyce, alyssa, alycia, alysha, alyson].map(x => x.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('フォロー中の非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||||
|
await createFollowings(root, [alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||||
|
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||||
|
|
||||||
|
const result = await service.search(
|
||||||
|
{ username: 'al' },
|
||||||
|
{ limit: 100 },
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
// alice, alyceはフォローしていないので後ろに行く
|
||||||
|
expect(result).toEqual([alycia, alysha, alyson, alyssa, alice, alyce].map(x => x.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('フォローしていないアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||||
|
await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||||
|
await setInactive([alice, alyce, alycia]);
|
||||||
|
|
||||||
|
const result = await service.search(
|
||||||
|
{ username: 'al' },
|
||||||
|
{ limit: 100 },
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
// alice, alyce, alyciaは非アクティブなので後ろに行く
|
||||||
|
expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('フォローしていない非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||||
|
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||||
|
|
||||||
|
const result = await service.search(
|
||||||
|
{ username: 'al' },
|
||||||
|
{ limit: 100 },
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('フォロー(アクティブ)、フォロー(非アクティブ)、非フォロー(アクティブ)、非フォロー(非アクティブ)混在時の優先順位度確認', async () => {
|
||||||
|
await createFollowings(root, [alyson, alyssa, bob, bobbi, bobbie]);
|
||||||
|
await setActive([root, alyssa, bob, bobbi, alyce, alycia]);
|
||||||
|
await setInactive([alyson, alice, alysha, bobbie, bobby]);
|
||||||
|
|
||||||
|
const result = await service.search(
|
||||||
|
{ },
|
||||||
|
{ limit: 100 },
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 見る用
|
||||||
|
// const users = await usersRepository.findBy({ id: In(result) }).then(it => new Map(it.map(x => [x.id, x])));
|
||||||
|
// console.log(result.map(x => users.get(x as any)).map(it => it?.username));
|
||||||
|
|
||||||
|
// フォローしててアクティブなので先頭: alyssa, bob, bobbi
|
||||||
|
// フォローしてて非アクティブなので次: alyson, bobbie
|
||||||
|
// フォローしてないけどアクティブなので次: alyce, alycia, root(アルファベット順的にここになる)
|
||||||
|
// フォローしてないし非アクティブなので最後: alice, alysha, bobby
|
||||||
|
expect(result).toEqual([alyssa, bob, bobbi, alyson, bobbie, alyce, alycia, root, alice, alysha, bobby].map(x => x.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[非ログイン] アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||||
|
await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||||
|
await setInactive([alice, alyce, alycia]);
|
||||||
|
|
||||||
|
const result = await service.search(
|
||||||
|
{ username: 'al' },
|
||||||
|
{ limit: 100 },
|
||||||
|
);
|
||||||
|
|
||||||
|
// alice, alyce, alyciaは非アクティブなので後ろに行く
|
||||||
|
expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[非ログイン] 非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||||
|
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||||
|
|
||||||
|
const result = await service.search(
|
||||||
|
{ username: 'al' },
|
||||||
|
{ limit: 100 },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('フォロー中のアクティブユーザのうち、"al"から始まり"example.com"にいる人が全員ヒットする', async () => {
|
||||||
|
await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||||
|
await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||||
|
|
||||||
|
const result = await service.search(
|
||||||
|
{ username: 'al', host: 'exam' },
|
||||||
|
{ limit: 100 },
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([alyson, alyssa].map(x => x.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('サスペンド済みユーザは出ない', async () => {
|
||||||
|
await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||||
|
await setSuspended([alice, alyce, alycia]);
|
||||||
|
|
||||||
|
const result = await service.search(
|
||||||
|
{ username: 'al' },
|
||||||
|
{ limit: 100 },
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([alysha, alyson, alyssa].map(x => x.id));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -18,6 +18,7 @@ import { entities } from '../src/postgres.js';
|
||||||
import { loadConfig } from '../src/config.js';
|
import { loadConfig } from '../src/config.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
import { type Response } from 'node-fetch';
|
import { type Response } from 'node-fetch';
|
||||||
|
import { ApiError } from "@/server/api/error.js";
|
||||||
|
|
||||||
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
|
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
|
||||||
|
|
||||||
|
@ -48,27 +49,28 @@ export const successfulApiCall = async <E extends keyof misskey.Endpoints, P ext
|
||||||
const res = await api(endpoint, parameters, user);
|
const res = await api(endpoint, parameters, user);
|
||||||
const status = assertion.status ?? (res.body == null ? 204 : 200);
|
const status = assertion.status ?? (res.body == null ? 204 : 200);
|
||||||
assert.strictEqual(res.status, status, inspect(res.body, { depth: 5, colors: true }));
|
assert.strictEqual(res.status, status, inspect(res.body, { depth: 5, colors: true }));
|
||||||
return res.body;
|
|
||||||
|
return res.body as misskey.api.SwitchCaseResponseType<E, P>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const failedApiCall = async <T, E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
|
export const failedApiCall = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
|
||||||
status: number,
|
status: number,
|
||||||
code: string,
|
code: string,
|
||||||
id: string
|
id: string
|
||||||
}): Promise<T> => {
|
}): Promise<void> => {
|
||||||
const { endpoint, parameters, user } = request;
|
const { endpoint, parameters, user } = request;
|
||||||
const { status, code, id } = assertion;
|
const { status, code, id } = assertion;
|
||||||
const res = await api(endpoint, parameters, user);
|
const res = await api(endpoint, parameters, user);
|
||||||
assert.strictEqual(res.status, status, inspect(res.body));
|
assert.strictEqual(res.status, status, inspect(res.body));
|
||||||
assert.strictEqual(res.body.error.code, code, inspect(res.body));
|
assert.ok(res.body);
|
||||||
assert.strictEqual(res.body.error.id, id, inspect(res.body));
|
assert.strictEqual(castAsError(res.body as any).error.code, code, inspect(res.body));
|
||||||
return res.body;
|
assert.strictEqual(castAsError(res.body as any).error.id, id, inspect(res.body));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const api = async <E extends keyof misskey.Endpoints>(path: E, params: misskey.Endpoints[E]['req'], me?: UserToken): Promise<{
|
export const api = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(path: E, params: P, me?: UserToken): Promise<{
|
||||||
status: number,
|
status: number,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
body: any
|
body: misskey.api.SwitchCaseResponseType<E, P>
|
||||||
}> => {
|
}> => {
|
||||||
const bodyAuth: Record<string, string> = {};
|
const bodyAuth: Record<string, string> = {};
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
|
@ -89,13 +91,14 @@ export const api = async <E extends keyof misskey.Endpoints>(path: E, params: mi
|
||||||
});
|
});
|
||||||
|
|
||||||
const body = res.headers.get('content-type') === 'application/json; charset=utf-8'
|
const body = res.headers.get('content-type') === 'application/json; charset=utf-8'
|
||||||
? await res.json()
|
? await res.json() as misskey.api.SwitchCaseResponseType<E, P>
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
headers: res.headers,
|
headers: res.headers,
|
||||||
body,
|
// FIXME: removing this non-null assertion: requires better typing around empty response.
|
||||||
|
body: body!,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -141,7 +144,8 @@ export const post = async (user: UserToken, params: misskey.Endpoints['notes/cre
|
||||||
|
|
||||||
const res = await api('notes/create', q, user);
|
const res = await api('notes/create', q, user);
|
||||||
|
|
||||||
return res.body ? res.body.createdNote : null;
|
// FIXME: the return type should reflect this fact.
|
||||||
|
return (res.body ? res.body.createdNote : null)!;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => {
|
export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => {
|
||||||
|
@ -635,3 +639,9 @@ export async function sendEnvResetRequest() {
|
||||||
throw new Error('server env update failed.');
|
throw new Error('server env update failed.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 与えられた値を強制的にエラーとみなす。この関数は型安全性を破壊するため、異常系のアサーション以外で用いられるべきではない。
|
||||||
|
// FIXME(misskey-js): misskey-jsがエラー情報を公開するようになったらこの関数を廃止する
|
||||||
|
export function castAsError(obj: Record<string, unknown>): { error: ApiError } {
|
||||||
|
return obj as { error: ApiError };
|
||||||
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
"@vitejs/plugin-vue": "5.0.5",
|
"@vitejs/plugin-vue": "5.0.5",
|
||||||
"@vue/compiler-sfc": "3.4.31",
|
"@vue/compiler-sfc": "3.4.31",
|
||||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.9",
|
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
|
||||||
"astring": "1.8.6",
|
"astring": "1.8.6",
|
||||||
"broadcast-channel": "7.0.0",
|
"broadcast-channel": "7.0.0",
|
||||||
"buraha": "0.0.1",
|
"buraha": "0.0.1",
|
||||||
|
|
|
@ -13,7 +13,6 @@ import * as sound from '@/scripts/sound.js';
|
||||||
import { $i, signout, updateAccount } from '@/account.js';
|
import { $i, signout, updateAccount } from '@/account.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||||
import { makeHotkey } from '@/scripts/hotkey.js';
|
|
||||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
||||||
|
@ -21,6 +20,7 @@ import { initializeSw } from '@/scripts/initialize-sw.js';
|
||||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||||
import { mainRouter } from '@/router/main.js';
|
import { mainRouter } from '@/router/main.js';
|
||||||
|
import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
|
||||||
|
|
||||||
export async function mainBoot() {
|
export async function mainBoot() {
|
||||||
const { isClientUpdated } = await common(() => createApp(
|
const { isClientUpdated } = await common(() => createApp(
|
||||||
|
@ -69,14 +69,6 @@ export async function mainBoot() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const hotkeys = {
|
|
||||||
'd': (): void => {
|
|
||||||
defaultStore.set('darkMode', !defaultStore.state.darkMode);
|
|
||||||
},
|
|
||||||
's': (): void => {
|
|
||||||
mainRouter.push('/search');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
if (defaultStore.state.enableSeasonalScreenEffect) {
|
if (defaultStore.state.enableSeasonalScreenEffect) {
|
||||||
const month = new Date().getMonth() + 1;
|
const month = new Date().getMonth() + 1;
|
||||||
|
@ -105,9 +97,6 @@ export async function mainBoot() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($i) {
|
if ($i) {
|
||||||
// only add post shortcuts if logged in
|
|
||||||
hotkeys['p|n'] = post;
|
|
||||||
|
|
||||||
defaultStore.loaded.then(() => {
|
defaultStore.loaded.then(() => {
|
||||||
if (defaultStore.state.accountSetupWizard !== -1) {
|
if (defaultStore.state.accountSetupWizard !== -1) {
|
||||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
|
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
|
||||||
|
@ -334,7 +323,19 @@ export async function mainBoot() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortcut
|
// shortcut
|
||||||
document.addEventListener('keydown', makeHotkey(hotkeys));
|
const keymap = {
|
||||||
|
'p|n': () => {
|
||||||
|
if ($i == null) return;
|
||||||
|
post();
|
||||||
|
},
|
||||||
|
'd': () => {
|
||||||
|
defaultStore.set('darkMode', !defaultStore.state.darkMode);
|
||||||
|
},
|
||||||
|
's': () => {
|
||||||
|
mainRouter.push('/search');
|
||||||
|
},
|
||||||
|
} as const satisfies Keymap;
|
||||||
|
document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
|
||||||
|
|
||||||
initializeSw();
|
initializeSw();
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,7 +153,7 @@ onMounted(() => {
|
||||||
background: linear-gradient(0deg, #ffee20, #eb7018);
|
background: linear-gradient(0deg, #ffee20, #eb7018);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -173,7 +173,7 @@ onMounted(() => {
|
||||||
background: linear-gradient(0deg, #e1e1e1, #7c7c7c);
|
background: linear-gradient(0deg, #e1e1e1, #7c7c7c);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -250,7 +250,6 @@ function onMousedown(evt: MouseEvent): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: solid 2px var(--focus);
|
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,17 +87,7 @@ async function onClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
&:after {
|
outline-offset: 2px;
|
||||||
content: "";
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
right: -5px;
|
|
||||||
bottom: -5px;
|
|
||||||
left: -5px;
|
|
||||||
border: 2px solid var(--focus);
|
|
||||||
border-radius: 32px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="position: relative;">
|
<div style="position: relative;">
|
||||||
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1" @click="updateLastReadedAt">
|
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" @click="updateLastReadedAt">
|
||||||
<div class="banner" :style="bannerStyle">
|
<div class="banner" :style="bannerStyle">
|
||||||
<div class="fade"></div>
|
<div class="fade"></div>
|
||||||
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
|
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
|
||||||
|
@ -80,6 +80,7 @@ const bannerStyle = computed(() => {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.eftoefju {
|
.eftoefju {
|
||||||
display: block;
|
display: block;
|
||||||
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@ -87,6 +88,22 @@ const bannerStyle = computed(() => {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: inset 0 0 0 2px var(--focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .banner {
|
> .banner {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -40,6 +40,14 @@ const remaining = computed(() => {
|
||||||
.link {
|
.link {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.root {
|
||||||
|
box-shadow: inset 0 0 0 2px var(--focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
||||||
>
|
>
|
||||||
<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
||||||
<MkMenu :items="items" :align="'left'" @close="$emit('closed')"/>
|
<MkMenu :items="items" :align="'left'" @close="emit('closed')"/>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -45,11 +45,11 @@ function toggle() {
|
||||||
.label {
|
.label {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: '(';
|
content: '(';
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
content: ')';
|
content: ')';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')">
|
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')" @esc="cancel()">
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div v-if="icon" :class="$style.icon">
|
<div v-if="icon" :class="$style.icon">
|
||||||
<i :class="icon"></i>
|
<i :class="icon"></i>
|
||||||
|
@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue';
|
import { ref, shallowRef, computed } from 'vue';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
@ -156,10 +156,6 @@ function onBgClick() {
|
||||||
if (props.cancelableByBgClick) cancel();
|
if (props.cancelableByBgClick) cancel();
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
function onKeydown(evt: KeyboardEvent) {
|
|
||||||
if (evt.key === 'Escape') cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onInputKeydown(evt: KeyboardEvent) {
|
function onInputKeydown(evt: KeyboardEvent) {
|
||||||
if (evt.key === 'Enter' && okButtonDisabledReason.value === null) {
|
if (evt.key === 'Enter' && okButtonDisabledReason.value === null) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
@ -167,14 +163,6 @@ function onInputKeydown(evt: KeyboardEvent) {
|
||||||
ok();
|
ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener('keydown', onKeydown);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
document.removeEventListener('keydown', onKeydown);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -115,14 +115,14 @@ function onDragend() {
|
||||||
background: rgba(#000, 0.05);
|
background: rgba(#000, 0.05);
|
||||||
|
|
||||||
> .label {
|
> .label {
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
background: #0b65a5;
|
background: #0b65a5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red {
|
&.red {
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
background: #c12113;
|
background: #c12113;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,14 +133,14 @@ function onDragend() {
|
||||||
background: rgba(#000, 0.1);
|
background: rgba(#000, 0.1);
|
||||||
|
|
||||||
> .label {
|
> .label {
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
background: #0b588c;
|
background: #0b588c;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red {
|
&.red {
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
background: #ce2212;
|
background: #ce2212;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,8 +159,8 @@ function onDragend() {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .label {
|
> .label {
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,8 +181,8 @@ function onDragend() {
|
||||||
left: 0;
|
left: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -190,14 +190,14 @@ function onDragend() {
|
||||||
background: #0c7ac9;
|
background: #0c7ac9;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 57px;
|
left: 57px;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
top: 57px;
|
top: 57px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
@ -205,8 +205,8 @@ function onDragend() {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red {
|
&.red {
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
background: #c12113;
|
background: #c12113;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -296,7 +296,7 @@ function onContextmenu(ev: MouseEvent) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.draghover {
|
&.draghover {
|
||||||
&:after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -5,7 +5,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
||||||
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" autocapitalize="off" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
|
<input
|
||||||
|
ref="searchEl"
|
||||||
|
:value="q"
|
||||||
|
class="search"
|
||||||
|
data-prevent-emoji-insert
|
||||||
|
:class="{ filled: q != null && q != '' }"
|
||||||
|
:placeholder="i18n.ts.search"
|
||||||
|
type="search"
|
||||||
|
autocapitalize="off"
|
||||||
|
@input="input()"
|
||||||
|
@paste.stop="paste"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
>
|
||||||
<!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
|
<!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
|
||||||
<div ref="emojisEl" class="emojis" tabindex="-1">
|
<div ref="emojisEl" class="emojis" tabindex="-1">
|
||||||
<section class="result">
|
<section class="result">
|
||||||
|
@ -139,6 +151,7 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'chosen', v: string): void;
|
(ev: 'chosen', v: string): void;
|
||||||
|
(ev: 'esc'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const searchEl = shallowRef<HTMLInputElement>();
|
const searchEl = shallowRef<HTMLInputElement>();
|
||||||
|
@ -433,9 +446,18 @@ function paste(event: ClipboardEvent): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEnter(ev: KeyboardEvent) {
|
function onKeydown(ev: KeyboardEvent) {
|
||||||
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
|
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
|
||||||
done();
|
if (ev.key === 'Enter') {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
if (ev.key === 'Escape') {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
emit('esc');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function done(query?: string): boolean | void {
|
function done(query?: string): boolean | void {
|
||||||
|
@ -702,11 +724,6 @@ defineExpose({
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: solid 2px var(--focus);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(0, 0, 0, 0.05);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
v-slot="{ type, maxHeight }"
|
v-slot="{ type, maxHeight }"
|
||||||
:zPriority="'middle'"
|
:zPriority="'middle'"
|
||||||
:preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
|
:preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
|
||||||
|
:hasInteractionWithOtherFocusTrappedEls="true"
|
||||||
:transparentBg="true"
|
:transparentBg="true"
|
||||||
:manualShowing="manualShowing"
|
:manualShowing="manualShowing"
|
||||||
:src="src"
|
:src="src"
|
||||||
@click="modal?.close()"
|
@click="modal?.close()"
|
||||||
|
@esc="modal?.close()"
|
||||||
@opening="opening"
|
@opening="opening"
|
||||||
@close="emit('close')"
|
@close="emit('close')"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
|
@ -28,6 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:asDrawer="type === 'drawer'"
|
:asDrawer="type === 'drawer'"
|
||||||
:max-height="maxHeight"
|
:max-height="maxHeight"
|
||||||
@chosen="chosen"
|
@chosen="chosen"
|
||||||
|
@esc="modal?.close()"
|
||||||
/>
|
/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel" tabindex="-1">
|
<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel">
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
<h1 :title="flash.title">{{ flash.title }}</h1>
|
<h1 :title="flash.title">{{ flash.title }}</h1>
|
||||||
|
@ -39,6 +39,10 @@ const props = defineProps<{
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
> article {
|
> article {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div ref="rootEl" :class="$style.root" role="group" :aria-expanded="opened">
|
<div ref="rootEl" :class="$style.root" role="group" :aria-expanded="opened">
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
|
<button :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
|
||||||
<div :class="$style.headerIcon"><slot name="icon"></slot></div>
|
<div :class="$style.headerIcon"><slot name="icon"></slot></div>
|
||||||
<div :class="$style.headerText">
|
<div :class="$style.headerText">
|
||||||
<div>
|
<div :class="$style.headerTextMain">
|
||||||
<MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine>
|
<MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.headerTextSub">
|
<div :class="$style.headerTextSub">
|
||||||
|
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i v-if="opened" class="ti ti-chevron-up icon"></i>
|
<i v-if="opened" class="ti ti-chevron-up icon"></i>
|
||||||
<i v-else class="ti ti-chevron-down icon"></i>
|
<i v-else class="ti ti-chevron-down icon"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
|
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
|
||||||
|
@ -151,6 +151,10 @@ onMounted(() => {
|
||||||
background: var(--buttonHoverBg);
|
background: var(--buttonHoverBg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background: var(--buttonHoverBg);
|
background: var(--buttonHoverBg);
|
||||||
|
@ -194,6 +198,12 @@ onMounted(() => {
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headerTextMain,
|
||||||
|
.headerTextSub {
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.headerTextSub {
|
.headerTextSub {
|
||||||
color: var(--fgTransparentWeak);
|
color: var(--fgTransparentWeak);
|
||||||
font-size: .85em;
|
font-size: .85em;
|
||||||
|
|
|
@ -42,6 +42,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
|
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
|
import { host } from '@/config.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
|
@ -63,7 +65,7 @@ const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFro
|
||||||
const wait = ref(false);
|
const wait = ref(false);
|
||||||
const connection = useStream().useChannel('main');
|
const connection = useStream().useChannel('main');
|
||||||
|
|
||||||
if (props.user.isFollowing == null) {
|
if (props.user.isFollowing == null && $i) {
|
||||||
misskeyApi('users/show', {
|
misskeyApi('users/show', {
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
})
|
})
|
||||||
|
@ -78,6 +80,8 @@ function onFollowChange(user: Misskey.entities.UserDetailed) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onClick() {
|
async function onClick() {
|
||||||
|
pleaseLogin(undefined, { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` });
|
||||||
|
|
||||||
wait.value = true;
|
wait.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -185,17 +189,7 @@ onBeforeUnmount(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
&:after {
|
outline-offset: 2px;
|
||||||
content: "";
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
right: -5px;
|
|
||||||
bottom: -5px;
|
|
||||||
left: -5px;
|
|
||||||
border: 2px solid var(--focus);
|
|
||||||
border-radius: 32px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -83,7 +83,7 @@ function leaveHover(): void {
|
||||||
|
|
||||||
> article {
|
> article {
|
||||||
> footer {
|
> footer {
|
||||||
&:before {
|
&::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,7 +139,7 @@ function leaveHover(): void {
|
||||||
text-shadow: 0 0 8px #000;
|
text-shadow: 0 0 8px #000;
|
||||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
|
:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
|
||||||
:leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
|
:leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
|
||||||
>
|
>
|
||||||
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined"/>
|
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/>
|
||||||
<img v-show="!hide" key="img" ref="img" :height="imgHeight" :width="imgWidth" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async"/>
|
<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')">
|
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()">
|
||||||
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
|
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<template v-for="item in items" :key="item.text">
|
<template v-for="item in items" :key="item.text">
|
||||||
|
|
|
@ -39,23 +39,37 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<audio
|
<audio
|
||||||
ref="audioEl"
|
ref="audioEl"
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
|
@keydown.prevent="() => {}"
|
||||||
>
|
>
|
||||||
<source :src="audio.url">
|
<source :src="audio.url">
|
||||||
</audio>
|
</audio>
|
||||||
<div :class="[$style.controlsChild, $style.controlsLeft]">
|
<div :class="[$style.controlsChild, $style.controlsLeft]">
|
||||||
<button class="_button" :class="$style.controlButton" @click="togglePlayPause">
|
<button
|
||||||
|
:class="['_button', $style.controlButton]"
|
||||||
|
tabindex="-1"
|
||||||
|
@click.stop="togglePlayPause"
|
||||||
|
>
|
||||||
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
|
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
|
||||||
<i v-else class="ti ti-player-play-filled"></i>
|
<i v-else class="ti ti-player-play-filled"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div :class="[$style.controlsChild, $style.controlsRight]">
|
<div :class="[$style.controlsChild, $style.controlsRight]">
|
||||||
<button class="_button" :class="$style.controlButton" @click="showMenu">
|
<button
|
||||||
|
:class="['_button', $style.controlButton]"
|
||||||
|
tabindex="-1"
|
||||||
|
@click.stop="() => {}"
|
||||||
|
@mousedown.prevent.stop="showMenu"
|
||||||
|
>
|
||||||
<i class="ti ti-settings"></i>
|
<i class="ti ti-settings"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
|
<div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
|
||||||
<div :class="[$style.controlsChild, $style.controlsVolume]">
|
<div :class="[$style.controlsChild, $style.controlsVolume]">
|
||||||
<button class="_button" :class="$style.controlButton" @click="toggleMute">
|
<button
|
||||||
|
:class="['_button', $style.controlButton]"
|
||||||
|
tabindex="-1"
|
||||||
|
@click.stop="toggleMute"
|
||||||
|
>
|
||||||
<i v-if="volume === 0" class="ti ti-volume-3"></i>
|
<i v-if="volume === 0" class="ti ti-volume-3"></i>
|
||||||
<i v-else class="ti ti-volume"></i>
|
<i v-else class="ti ti-volume"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -80,6 +94,7 @@ import type { MenuItem } from '@/types/menu.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
import { type Keymap } from '@/scripts/hotkey.js';
|
||||||
import bytes from '@/filters/bytes.js';
|
import bytes from '@/filters/bytes.js';
|
||||||
import { hms } from '@/filters/hms.js';
|
import { hms } from '@/filters/hms.js';
|
||||||
import MkMediaRange from '@/components/MkMediaRange.vue';
|
import MkMediaRange from '@/components/MkMediaRange.vue';
|
||||||
|
@ -90,32 +105,44 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'up': () => {
|
'up': {
|
||||||
if (hasFocus() && audioEl.value) {
|
allowRepeat: true,
|
||||||
volume.value = Math.min(volume.value + 0.1, 1);
|
callback: () => {
|
||||||
}
|
if (hasFocus() && audioEl.value) {
|
||||||
|
volume.value = Math.min(volume.value + 0.1, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'down': () => {
|
'down': {
|
||||||
if (hasFocus() && audioEl.value) {
|
allowRepeat: true,
|
||||||
volume.value = Math.max(volume.value - 0.1, 0);
|
callback: () => {
|
||||||
}
|
if (hasFocus() && audioEl.value) {
|
||||||
|
volume.value = Math.max(volume.value - 0.1, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'left': () => {
|
'left': {
|
||||||
if (hasFocus() && audioEl.value) {
|
allowRepeat: true,
|
||||||
audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0);
|
callback: () => {
|
||||||
}
|
if (hasFocus() && audioEl.value) {
|
||||||
|
audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'right': () => {
|
'right': {
|
||||||
if (hasFocus() && audioEl.value) {
|
allowRepeat: true,
|
||||||
audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration);
|
callback: () => {
|
||||||
}
|
if (hasFocus() && audioEl.value) {
|
||||||
|
audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'space': () => {
|
'space': () => {
|
||||||
if (hasFocus()) {
|
if (hasFocus()) {
|
||||||
togglePlayPause();
|
togglePlayPause();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
} as const satisfies Keymap;
|
||||||
|
|
||||||
// PlayerElもしくはその子要素にフォーカスがあるかどうか
|
// PlayerElもしくはその子要素にフォーカスがあるかどうか
|
||||||
function hasFocus() {
|
function hasFocus() {
|
||||||
|
@ -358,7 +385,7 @@ onDeactivated(() => {
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
|
||||||
&:focus {
|
&:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -424,6 +451,10 @@ onDeactivated(() => {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background-color: var(--accentedBg);
|
background-color: var(--accentedBg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ import XVideo from '@/components/MkMediaVideo.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { focusParent } from '@/scripts/focus.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mediaList: Misskey.entities.DriveFile[];
|
mediaList: Misskey.entities.DriveFile[];
|
||||||
|
@ -49,7 +50,9 @@ const gallery = shallowRef<HTMLDivElement>();
|
||||||
const pswpZIndex = os.claimZIndex('middle');
|
const pswpZIndex = os.claimZIndex('middle');
|
||||||
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
|
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
|
||||||
const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
|
const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
|
||||||
let lightbox: PhotoSwipeLightbox | null;
|
let lightbox: PhotoSwipeLightbox | null = null;
|
||||||
|
|
||||||
|
let activeEl: HTMLElement | null = null;
|
||||||
|
|
||||||
const popstateHandler = (): void => {
|
const popstateHandler = (): void => {
|
||||||
if (lightbox?.pswp && lightbox.pswp.isOpen === true) {
|
if (lightbox?.pswp && lightbox.pswp.isOpen === true) {
|
||||||
|
@ -60,7 +63,7 @@ const popstateHandler = (): void => {
|
||||||
async function calcAspectRatio() {
|
async function calcAspectRatio() {
|
||||||
if (!gallery.value) return;
|
if (!gallery.value) return;
|
||||||
|
|
||||||
let img = props.mediaList[0];
|
const img = props.mediaList[0];
|
||||||
|
|
||||||
if (props.mediaList.length !== 1 || !(img.properties.width && img.properties.height)) {
|
if (props.mediaList.length !== 1 || !(img.properties.width && img.properties.height)) {
|
||||||
gallery.value.style.aspectRatio = '';
|
gallery.value.style.aspectRatio = '';
|
||||||
|
@ -131,6 +134,7 @@ onMounted(() => {
|
||||||
bgOpacity: 1,
|
bgOpacity: 1,
|
||||||
showAnimationDuration: 100,
|
showAnimationDuration: 100,
|
||||||
hideAnimationDuration: 100,
|
hideAnimationDuration: 100,
|
||||||
|
returnFocus: false,
|
||||||
pswpModule: PhotoSwipe,
|
pswpModule: PhotoSwipe,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -159,39 +163,47 @@ onMounted(() => {
|
||||||
lightbox.on('uiRegister', () => {
|
lightbox.on('uiRegister', () => {
|
||||||
lightbox?.pswp?.ui?.registerElement({
|
lightbox?.pswp?.ui?.registerElement({
|
||||||
name: 'altText',
|
name: 'altText',
|
||||||
className: 'pwsp__alt-text-container',
|
className: 'pswp__alt-text-container',
|
||||||
appendTo: 'wrapper',
|
appendTo: 'wrapper',
|
||||||
onInit: (el, pwsp) => {
|
onInit: (el, pswp) => {
|
||||||
let textBox = document.createElement('p');
|
const textBox = document.createElement('p');
|
||||||
textBox.className = 'pwsp__alt-text _acrylic';
|
textBox.className = 'pswp__alt-text _acrylic';
|
||||||
el.appendChild(textBox);
|
el.appendChild(textBox);
|
||||||
|
|
||||||
pwsp.on('change', () => {
|
pswp.on('change', () => {
|
||||||
textBox.textContent = pwsp.currSlide?.data.comment;
|
textBox.textContent = pswp.currSlide?.data.comment;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
lightbox.init();
|
lightbox.on('afterInit', () => {
|
||||||
|
activeEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||||
window.addEventListener('popstate', popstateHandler);
|
focusParent(activeEl, true, true);
|
||||||
|
lightbox?.pswp?.element?.focus({
|
||||||
lightbox.on('beforeOpen', () => {
|
preventScroll: true,
|
||||||
|
});
|
||||||
history.pushState(null, '', '#pswp');
|
history.pushState(null, '', '#pswp');
|
||||||
});
|
});
|
||||||
|
|
||||||
lightbox.on('close', () => {
|
lightbox.on('destroy', () => {
|
||||||
|
focusParent(activeEl, true, false);
|
||||||
|
activeEl = null;
|
||||||
if (window.location.hash === '#pswp') {
|
if (window.location.hash === '#pswp') {
|
||||||
history.back();
|
history.back();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('popstate', popstateHandler);
|
||||||
|
|
||||||
|
lightbox.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('popstate', popstateHandler);
|
window.removeEventListener('popstate', popstateHandler);
|
||||||
lightbox?.destroy();
|
lightbox?.destroy();
|
||||||
lightbox = null;
|
lightbox = null;
|
||||||
|
activeEl = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
||||||
|
@ -199,6 +211,16 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
||||||
// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
|
// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
|
||||||
return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
|
return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openGallery = () => {
|
||||||
|
if (props.mediaList.filter(media => previewable(media)).length > 0) {
|
||||||
|
lightbox?.loadAndOpen(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openGallery,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -298,7 +320,7 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
||||||
backdrop-filter: var(--modalBgFilter);
|
backdrop-filter: var(--modalBgFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pwsp__alt-text-container {
|
.pswp__alt-text-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -312,7 +334,7 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pwsp__alt-text {
|
.pswp__alt-text {
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -112,6 +112,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue';
|
import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
|
import { type Keymap } from '@/scripts/hotkey.js';
|
||||||
import bytes from '@/filters/bytes.js';
|
import bytes from '@/filters/bytes.js';
|
||||||
import { hms } from '@/filters/hms.js';
|
import { hms } from '@/filters/hms.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
@ -127,32 +128,44 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'up': () => {
|
'up': {
|
||||||
if (hasFocus() && videoEl.value) {
|
allowRepeat: true,
|
||||||
volume.value = Math.min(volume.value + 0.1, 1);
|
callback: () => {
|
||||||
}
|
if (hasFocus() && videoEl.value) {
|
||||||
|
volume.value = Math.min(volume.value + 0.1, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'down': () => {
|
'down': {
|
||||||
if (hasFocus() && videoEl.value) {
|
allowRepeat: true,
|
||||||
volume.value = Math.max(volume.value - 0.1, 0);
|
callback: () => {
|
||||||
}
|
if (hasFocus() && videoEl.value) {
|
||||||
|
volume.value = Math.max(volume.value - 0.1, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'left': () => {
|
'left': {
|
||||||
if (hasFocus() && videoEl.value) {
|
allowRepeat: true,
|
||||||
videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0);
|
callback: () => {
|
||||||
}
|
if (hasFocus() && videoEl.value) {
|
||||||
|
videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'right': () => {
|
'right': {
|
||||||
if (hasFocus() && videoEl.value) {
|
allowRepeat: true,
|
||||||
videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration);
|
callback: () => {
|
||||||
}
|
if (hasFocus() && videoEl.value) {
|
||||||
|
videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'space': () => {
|
'space': () => {
|
||||||
if (hasFocus()) {
|
if (hasFocus()) {
|
||||||
togglePlayPause();
|
togglePlayPause();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
} as const satisfies Keymap;
|
||||||
|
|
||||||
// PlayerElもしくはその子要素にフォーカスがあるかどうか
|
// PlayerElもしくはその子要素にフォーカスがあるかどうか
|
||||||
function hasFocus() {
|
function hasFocus() {
|
||||||
|
@ -468,7 +481,7 @@ onDeactivated(() => {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
|
||||||
&:focus {
|
&:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -575,6 +588,10 @@ onDeactivated(() => {
|
||||||
border-radius: 99rem;
|
border-radius: 99rem;
|
||||||
|
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.videoLoading {
|
.videoLoading {
|
||||||
|
@ -638,6 +655,10 @@ onDeactivated(() => {
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--accent);
|
background-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
|
import { nextTick, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue';
|
||||||
import MkMenu from './MkMenu.vue';
|
import MkMenu from './MkMenu.vue';
|
||||||
import { MenuItem } from '@/types/menu.js';
|
import { MenuItem } from '@/types/menu.js';
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ const props = defineProps<{
|
||||||
targetElement: HTMLElement;
|
targetElement: HTMLElement;
|
||||||
rootElement: HTMLElement;
|
rootElement: HTMLElement;
|
||||||
width?: number;
|
width?: number;
|
||||||
viaKeyboard?: boolean;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -27,6 +26,8 @@ const emit = defineEmits<{
|
||||||
(ev: 'actioned'): void;
|
(ev: 'actioned'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
provide('isNestingMenu', true);
|
||||||
|
|
||||||
const el = shallowRef<HTMLElement>();
|
const el = shallowRef<HTMLElement>();
|
||||||
const align = 'left';
|
const align = 'left';
|
||||||
|
|
||||||
|
|
|
@ -4,23 +4,42 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div role="menu">
|
<div role="menu" @focusin.passive.stop="() => {}">
|
||||||
<div
|
<div
|
||||||
ref="itemsEl" v-hotkey="keymap"
|
ref="itemsEl"
|
||||||
|
v-hotkey="keymap"
|
||||||
|
tabindex="0"
|
||||||
class="_popup _shadow"
|
class="_popup _shadow"
|
||||||
:class="[$style.root, { [$style.center]: align === 'center', [$style.asDrawer]: asDrawer }]"
|
:class="{
|
||||||
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
|
[$style.root]: true,
|
||||||
@contextmenu.self="e => e.preventDefault()"
|
[$style.center]: align === 'center',
|
||||||
|
[$style.asDrawer]: asDrawer,
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
width: (width && !asDrawer) ? `${width}px` : '',
|
||||||
|
maxHeight: maxHeight ? `${maxHeight}px` : '',
|
||||||
|
}"
|
||||||
|
@keydown.stop="() => {}"
|
||||||
|
@contextmenu.self.prevent="() => {}"
|
||||||
>
|
>
|
||||||
<template v-for="(item, i) in (items2 ?? [])">
|
<template v-for="item in (items2 ?? [])">
|
||||||
<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
|
<div v-if="item.type === 'divider'" role="separator" tabindex="-1" :class="$style.divider"></div>
|
||||||
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
|
<span v-else-if="item.type === 'label'" role="menuitem" tabindex="-1" :class="[$style.label, $style.item]">
|
||||||
<span style="opacity: 0.7;">{{ item.text }}</span>
|
<span style="opacity: 0.7;">{{ item.text }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]">
|
<span v-else-if="item.type === 'pending'" role="menuitem" tabindex="0" :class="[$style.pending, $style.item]">
|
||||||
<span><MkEllipsis/></span>
|
<span><MkEllipsis/></span>
|
||||||
</span>
|
</span>
|
||||||
<MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<MkA
|
||||||
|
v-else-if="item.type === 'link'"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item]"
|
||||||
|
:to="item.to"
|
||||||
|
@click.passive="close(true)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter"
|
||||||
|
@mouseleave.passive="onItemMouseLeave"
|
||||||
|
>
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||||
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
|
@ -28,20 +47,49 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</MkA>
|
</MkA>
|
||||||
<a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<a
|
||||||
|
v-else-if="item.type === 'a'"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item]"
|
||||||
|
:href="item.href"
|
||||||
|
:target="item.target"
|
||||||
|
:rel="item.target === '_blank' ? 'noopener noreferrer' : undefined"
|
||||||
|
:download="item.download"
|
||||||
|
@click.passive="close(true)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter"
|
||||||
|
@mouseleave.passive="onItemMouseLeave"
|
||||||
|
>
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
<span :class="$style.item_content_text">{{ item.text }}</span>
|
<span :class="$style.item_content_text">{{ item.text }}</span>
|
||||||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<button
|
||||||
|
v-else-if="item.type === 'user'"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item, { [$style.active]: item.active }]"
|
||||||
|
@click.prevent="item.active ? close(false) : clicked(item.action, $event)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter"
|
||||||
|
@mouseleave.passive="onItemMouseLeave"
|
||||||
|
>
|
||||||
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
|
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
|
||||||
<div v-if="item.indicate" :class="$style.item_content">
|
<div v-if="item.indicate" :class="$style.item_content">
|
||||||
<span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
<span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<button
|
||||||
|
v-else-if="item.type === 'switch'"
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item]"
|
||||||
|
:disabled="unref(item.disabled)"
|
||||||
|
@click.prevent="switchItem(item)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter"
|
||||||
|
@mouseleave.passive="onItemMouseLeave"
|
||||||
|
>
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||||
<MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
<MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
|
@ -49,29 +97,61 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
<MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="item.type === 'radio'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showRadioOptions(item, $event)" @click="!preferClick ? null : showRadioOptions(item, $event)">
|
<button
|
||||||
|
v-else-if="item.type === 'radio'"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]"
|
||||||
|
:disabled="unref(item.disabled)"
|
||||||
|
@mouseenter.prevent="preferClick ? null : showRadioOptions(item, $event)"
|
||||||
|
@keydown.enter.prevent="preferClick ? null : showRadioOptions(item, $event)"
|
||||||
|
@click.prevent="!preferClick ? null : showRadioOptions(item, $event)"
|
||||||
|
>
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
|
<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
|
||||||
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="item.type === 'radioOption'" :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.radioActive]: item.active }]" @click="clicked(item.action, $event, false)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<button
|
||||||
|
v-else-if="item.type === 'radioOption'"
|
||||||
|
role="menuitemradio"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item, $style.radio, { [$style.active]: unref(item.active) }]"
|
||||||
|
@click.prevent="unref(item.active) ? null : clicked(item.action, $event, false)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter"
|
||||||
|
@mouseleave.passive="onItemMouseLeave"
|
||||||
|
>
|
||||||
<div :class="$style.icon">
|
<div :class="$style.icon">
|
||||||
<span :class="[$style.radio, { [$style.radioChecked]: item.active }]"></span>
|
<span :class="[$style.radioIcon, { [$style.radioChecked]: unref(item.active) }]"></span>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
<span :class="$style.item_content_text">{{ item.text }}</span>
|
<span :class="$style.item_content_text">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
|
<button
|
||||||
|
v-else-if="item.type === 'parent'"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]"
|
||||||
|
@mouseenter.prevent="preferClick ? null : showChildren(item, $event)"
|
||||||
|
@keydown.enter.prevent="preferClick ? null : showChildren(item, $event)"
|
||||||
|
@click.prevent="!preferClick ? null : showChildren(item, $event)"
|
||||||
|
>
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
|
<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
|
||||||
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }]" :disabled="getValue(item.active)" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<button
|
||||||
|
v-else role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]"
|
||||||
|
@click.prevent="unref(item.active) ? close(false) : clicked(item.action, $event)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter"
|
||||||
|
@mouseleave.passive="onItemMouseLeave"
|
||||||
|
>
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||||
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
|
@ -80,24 +160,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<span v-if="items2 == null || items2.length === 0" :class="[$style.none, $style.item]">
|
<span v-if="items2 == null || items2.length === 0" tabindex="-1" :class="[$style.none, $style.item]">
|
||||||
<span>{{ i18n.ts.none }}</span>
|
<span>{{ i18n.ts.none }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="childMenu">
|
<div v-if="childMenu">
|
||||||
<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" showing @actioned="childActioned" @close="close(false)"/>
|
<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" @actioned="childActioned" @closed="closeChild"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
|
||||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
||||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
|
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { isTouchUsing } from '@/scripts/touch.js';
|
import { isTouchUsing } from '@/scripts/touch.js';
|
||||||
|
import { type Keymap } from '@/scripts/hotkey.js';
|
||||||
|
import { isFocusable } from '@/scripts/focus.js';
|
||||||
|
import { getNodeOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||||
|
|
||||||
const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
|
const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
|
||||||
</script>
|
</script>
|
||||||
|
@ -107,7 +189,6 @@ const XChild = defineAsyncComponent(() => import('./MkMenu.child.vue'));
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
viaKeyboard?: boolean;
|
|
||||||
asDrawer?: boolean;
|
asDrawer?: boolean;
|
||||||
align?: 'center' | string;
|
align?: 'center' | string;
|
||||||
width?: number;
|
width?: number;
|
||||||
|
@ -119,17 +200,28 @@ const emit = defineEmits<{
|
||||||
(ev: 'hide'): void;
|
(ev: 'hide'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const itemsEl = shallowRef<HTMLDivElement>();
|
const isNestingMenu = inject<boolean>('isNestingMenu', false);
|
||||||
|
|
||||||
|
const itemsEl = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const items2 = ref<InnerMenuItem[]>();
|
const items2 = ref<InnerMenuItem[]>();
|
||||||
|
|
||||||
const child = shallowRef<InstanceType<typeof XChild>>();
|
const child = shallowRef<InstanceType<typeof XChild>>();
|
||||||
|
|
||||||
const keymap = computed(() => ({
|
const keymap = {
|
||||||
'up|k|shift+tab': focusUp,
|
'up|k|shift+tab': {
|
||||||
'down|j|tab': focusDown,
|
allowRepeat: true,
|
||||||
'esc': close,
|
callback: () => focusUp(),
|
||||||
}));
|
},
|
||||||
|
'down|j|tab': {
|
||||||
|
allowRepeat: true,
|
||||||
|
callback: () => focusDown(),
|
||||||
|
},
|
||||||
|
'esc': {
|
||||||
|
allowRepeat: true,
|
||||||
|
callback: () => close(false),
|
||||||
|
},
|
||||||
|
} as const satisfies Keymap;
|
||||||
|
|
||||||
const childShowingItem = ref<MenuItem | null>();
|
const childShowingItem = ref<MenuItem | null>();
|
||||||
|
|
||||||
|
@ -167,25 +259,19 @@ function childActioned() {
|
||||||
close(true);
|
close(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onGlobalMousedown = (event: MouseEvent) => {
|
|
||||||
if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target as Node))) return;
|
|
||||||
if (child.value && child.value.checkHit(event)) return;
|
|
||||||
closeChild();
|
|
||||||
};
|
|
||||||
|
|
||||||
let childCloseTimer: null | number = null;
|
let childCloseTimer: null | number = null;
|
||||||
|
|
||||||
function onItemMouseEnter(item) {
|
function onItemMouseEnter() {
|
||||||
childCloseTimer = window.setTimeout(() => {
|
childCloseTimer = window.setTimeout(() => {
|
||||||
closeChild();
|
closeChild();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onItemMouseLeave(item) {
|
function onItemMouseLeave() {
|
||||||
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
|
async function showRadioOptions(item: MenuRadio, ev: Event) {
|
||||||
const children: MenuItem[] = Object.keys(item.options).map<MenuRadioOption>(key => {
|
const children: MenuItem[] = Object.keys(item.options).map<MenuRadioOption>(key => {
|
||||||
const value = item.options[key];
|
const value = item.options[key];
|
||||||
return {
|
return {
|
||||||
|
@ -200,7 +286,7 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
|
||||||
|
|
||||||
if (props.asDrawer) {
|
if (props.asDrawer) {
|
||||||
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
|
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
|
||||||
emit('close');
|
close(false);
|
||||||
});
|
});
|
||||||
emit('hide');
|
emit('hide');
|
||||||
} else {
|
} else {
|
||||||
|
@ -210,7 +296,7 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showChildren(item: MenuParent, ev: MouseEvent) {
|
async function showChildren(item: MenuParent, ev: Event) {
|
||||||
const children: MenuItem[] = await (async () => {
|
const children: MenuItem[] = await (async () => {
|
||||||
if (childrenCache.has(item)) {
|
if (childrenCache.has(item)) {
|
||||||
return childrenCache.get(item)!;
|
return childrenCache.get(item)!;
|
||||||
|
@ -227,7 +313,7 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
|
||||||
|
|
||||||
if (props.asDrawer) {
|
if (props.asDrawer) {
|
||||||
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
|
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
|
||||||
emit('close');
|
close(false);
|
||||||
});
|
});
|
||||||
emit('hide');
|
emit('hide');
|
||||||
} else {
|
} else {
|
||||||
|
@ -246,15 +332,11 @@ function clicked(fn: MenuAction, ev: MouseEvent, doClose = true) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(actioned = false) {
|
function close(actioned = false) {
|
||||||
emit('close', actioned);
|
disposeHandlers();
|
||||||
}
|
nextTick(() => {
|
||||||
|
closeChild();
|
||||||
function focusUp() {
|
emit('close', actioned);
|
||||||
focusPrev(document.activeElement);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function focusDown() {
|
|
||||||
focusNext(document.activeElement);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchItem(item: MenuSwitch & { ref: any }) {
|
function switchItem(item: MenuSwitch & { ref: any }) {
|
||||||
|
@ -262,25 +344,75 @@ function switchItem(item: MenuSwitch & { ref: any }) {
|
||||||
item.ref = !item.ref;
|
item.ref = !item.ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getValue<T>(item?: ComputedRef<T> | T) {
|
function focusUp() {
|
||||||
return isRef(item) ? item.value : item;
|
if (disposed) return;
|
||||||
|
if (!itemsEl.value?.contains(document.activeElement)) return;
|
||||||
|
|
||||||
|
const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
|
||||||
|
const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
|
||||||
|
const targetIndex = (activeIndex !== -1 && activeIndex !== 0) ? (activeIndex - 1) : (focusableElements.length - 1);
|
||||||
|
const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
|
||||||
|
|
||||||
|
targetElement.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
function focusDown() {
|
||||||
if (props.viaKeyboard) {
|
if (disposed) return;
|
||||||
nextTick(() => {
|
if (!itemsEl.value?.contains(document.activeElement)) return;
|
||||||
if (itemsEl.value) focusNext(itemsEl.value.children[0], true, false);
|
|
||||||
});
|
const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
|
||||||
|
const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
|
||||||
|
const targetIndex = (activeIndex !== -1 && activeIndex !== (focusableElements.length - 1)) ? (activeIndex + 1) : 0;
|
||||||
|
const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
|
||||||
|
|
||||||
|
targetElement.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGlobalFocusin = (ev: FocusEvent) => {
|
||||||
|
if (disposed) return;
|
||||||
|
if (itemsEl.value?.parentElement?.contains(getNodeOrNull(ev.target))) return;
|
||||||
|
nextTick(() => {
|
||||||
|
if (itemsEl.value != null && isFocusable(itemsEl.value)) {
|
||||||
|
itemsEl.value.focus({ preventScroll: true });
|
||||||
|
nextTick(() => focusDown());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGlobalMousedown = (ev: MouseEvent) => {
|
||||||
|
if (disposed) return;
|
||||||
|
if (childTarget.value?.contains(getNodeOrNull(ev.target))) return;
|
||||||
|
if (child.value?.checkHit(ev)) return;
|
||||||
|
closeChild();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupHandlers = () => {
|
||||||
|
if (!isNestingMenu) {
|
||||||
|
document.addEventListener('focusin', onGlobalFocusin, { passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: アクティブな要素までスクロール
|
|
||||||
//itemsEl.scrollTo();
|
|
||||||
|
|
||||||
document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
|
document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
let disposed = false;
|
||||||
|
|
||||||
|
const disposeHandlers = () => {
|
||||||
|
disposed = true;
|
||||||
|
if (!isNestingMenu) {
|
||||||
|
document.removeEventListener('focusin', onGlobalFocusin);
|
||||||
|
}
|
||||||
|
document.removeEventListener('mousedown', onGlobalMousedown);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setupHandlers();
|
||||||
|
|
||||||
|
if (!isNestingMenu) {
|
||||||
|
nextTick(() => itemsEl.value?.focus({ preventScroll: true }));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener('mousedown', onGlobalMousedown);
|
disposeHandlers();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -293,6 +425,10 @@ onBeforeUnmount(() => {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
&.center {
|
&.center {
|
||||||
> .item {
|
> .item {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -310,7 +446,7 @@ onBeforeUnmount(() => {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
width: calc(100% - 24px);
|
width: calc(100% - 24px);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
@ -340,8 +476,10 @@ onBeforeUnmount(() => {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-decoration: none !important;
|
||||||
|
color: var(--menuFg, var(--fg));
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -355,56 +493,56 @@ onBeforeUnmount(() => {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:disabled):hover {
|
&:focus-visible {
|
||||||
color: var(--accent);
|
outline: none;
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:before {
|
&:not(:hover):not(:active)::before {
|
||||||
background: var(--accentedBg);
|
outline: var(--focus) solid 2px;
|
||||||
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(:disabled) {
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible:active,
|
||||||
|
&:focus-visible.active {
|
||||||
|
color: var(--menuHoverFg, var(--accent));
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: var(--menuHoverBg, var(--accentedBg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:focus-visible):active,
|
||||||
|
&:not(:focus-visible).active {
|
||||||
|
color: var(--menuActiveFg, var(--fgOnAccent));
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: var(--menuActiveBg, var(--accent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
&.danger {
|
&.danger {
|
||||||
color: #ff2a2a;
|
--menuFg: #ff2a2a;
|
||||||
|
--menuHoverFg: #fff;
|
||||||
&:hover {
|
--menuHoverBg: #ff4242;
|
||||||
color: #fff;
|
--menuActiveFg: #fff;
|
||||||
|
--menuActiveBg: #d42e2e;
|
||||||
&:before {
|
|
||||||
background: #ff4242;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
background: #d42e2e !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active,
|
&.radio {
|
||||||
&.active {
|
--menuActiveFg: var(--accent);
|
||||||
color: var(--fgOnAccent) !important;
|
--menuActiveBg: var(--accentedBg);
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
background: var(--accent) !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.radioActive {
|
&.parent {
|
||||||
color: var(--accent) !important;
|
--menuActiveFg: var(--accent);
|
||||||
opacity: 1;
|
--menuActiveBg: var(--accentedBg);
|
||||||
|
|
||||||
&:before {
|
|
||||||
background-color: var(--accentedBg) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(:active):focus-visible {
|
|
||||||
box-shadow: 0 0 0 2px var(--focus) inset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.label {
|
&.label {
|
||||||
|
@ -422,22 +560,6 @@ onBeforeUnmount(() => {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.parent {
|
|
||||||
pointer-events: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: default;
|
|
||||||
|
|
||||||
&.childShowing {
|
|
||||||
color: var(--accent);
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
background: var(--accentedBg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item_content {
|
.item_content {
|
||||||
|
@ -456,18 +578,6 @@ onBeforeUnmount(() => {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switchDisabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switchButton {
|
.switchButton {
|
||||||
margin-left: -2px;
|
margin-left: -2px;
|
||||||
--height: 1.35em;
|
--height: 1.35em;
|
||||||
|
@ -479,14 +589,6 @@ onBeforeUnmount(() => {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switchInput {
|
|
||||||
position: absolute;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
@ -515,12 +617,12 @@ onBeforeUnmount(() => {
|
||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio {
|
.radioIcon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
vertical-align: -.125em;
|
vertical-align: -0.125em;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: solid 2px var(--divider);
|
border: solid 2px var(--divider);
|
||||||
background-color: var(--panel);
|
background-color: var(--panel);
|
||||||
|
|
|
@ -30,9 +30,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
[$style.transition_modal_leaveTo]: transitionName === 'modal',
|
[$style.transition_modal_leaveTo]: transitionName === 'modal',
|
||||||
[$style.transition_send_leaveTo]: transitionName === 'send',
|
[$style.transition_send_leaveTo]: transitionName === 'send',
|
||||||
})"
|
})"
|
||||||
:duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened"
|
:duration="transitionDuration" appear @afterLeave="onClosed" @enter="emit('opening')" @afterEnter="onOpened"
|
||||||
>
|
>
|
||||||
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
<div v-show="manualShowing != null ? manualShowing : showing" ref="modalRootEl" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||||
<div data-cy-bg :data-cy-transparent="isEnableBgTransparent" class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
<div data-cy-bg :data-cy-transparent="isEnableBgTransparent" class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||||
<div ref="content" :class="[$style.content, { [$style.fixed]: fixed }]" :style="{ zIndex }" @click.self="onBgClick">
|
<div ref="content" :class="[$style.content, { [$style.fixed]: fixed }]" :style="{ zIndex }" @click.self="onBgClick">
|
||||||
<slot :max-height="maxHeight" :type="type"></slot>
|
<slot :max-height="maxHeight" :type="type"></slot>
|
||||||
|
@ -47,6 +47,9 @@ import * as os from '@/os.js';
|
||||||
import { isTouchUsing } from '@/scripts/touch.js';
|
import { isTouchUsing } from '@/scripts/touch.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
|
import { type Keymap } from '@/scripts/hotkey.js';
|
||||||
|
import { focusTrap } from '@/scripts/focus-trap.js';
|
||||||
|
import { focusParent } from '@/scripts/focus.js';
|
||||||
|
|
||||||
function getFixedContainer(el: Element | null): Element | null {
|
function getFixedContainer(el: Element | null): Element | null {
|
||||||
if (el == null || el.tagName === 'BODY') return null;
|
if (el == null || el.tagName === 'BODY') return null;
|
||||||
|
@ -68,6 +71,8 @@ const props = withDefaults(defineProps<{
|
||||||
zPriority?: 'low' | 'middle' | 'high';
|
zPriority?: 'low' | 'middle' | 'high';
|
||||||
noOverlap?: boolean;
|
noOverlap?: boolean;
|
||||||
transparentBg?: boolean;
|
transparentBg?: boolean;
|
||||||
|
hasInteractionWithOtherFocusTrappedEls?: boolean;
|
||||||
|
returnFocusTo?: HTMLElement | null;
|
||||||
}>(), {
|
}>(), {
|
||||||
manualShowing: null,
|
manualShowing: null,
|
||||||
src: null,
|
src: null,
|
||||||
|
@ -76,6 +81,8 @@ const props = withDefaults(defineProps<{
|
||||||
zPriority: 'low',
|
zPriority: 'low',
|
||||||
noOverlap: true,
|
noOverlap: true,
|
||||||
transparentBg: false,
|
transparentBg: false,
|
||||||
|
hasInteractionWithOtherFocusTrappedEls: false,
|
||||||
|
returnFocusTo: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -93,6 +100,7 @@ const maxHeight = ref<number>();
|
||||||
const fixed = ref(false);
|
const fixed = ref(false);
|
||||||
const transformOrigin = ref('center');
|
const transformOrigin = ref('center');
|
||||||
const showing = ref(true);
|
const showing = ref(true);
|
||||||
|
const modalRootEl = shallowRef<HTMLElement>();
|
||||||
const content = shallowRef<HTMLElement>();
|
const content = shallowRef<HTMLElement>();
|
||||||
const zIndex = os.claimZIndex(props.zPriority);
|
const zIndex = os.claimZIndex(props.zPriority);
|
||||||
const useSendAnime = ref(false);
|
const useSendAnime = ref(false);
|
||||||
|
@ -131,6 +139,7 @@ const transitionDuration = computed((() =>
|
||||||
: 0
|
: 0
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let releaseFocusTrap: (() => void) | null = null;
|
||||||
let contentClicking = false;
|
let contentClicking = false;
|
||||||
|
|
||||||
function close(opts: { useSendAnimation?: boolean } = {}) {
|
function close(opts: { useSendAnimation?: boolean } = {}) {
|
||||||
|
@ -154,8 +163,11 @@ if (type.value === 'drawer') {
|
||||||
}
|
}
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'esc': () => emit('esc'),
|
'esc': {
|
||||||
};
|
allowRepeat: true,
|
||||||
|
callback: () => emit('esc'),
|
||||||
|
},
|
||||||
|
} as const satisfies Keymap;
|
||||||
|
|
||||||
const MARGIN = 16;
|
const MARGIN = 16;
|
||||||
const SCROLLBAR_THICKNESS = 16;
|
const SCROLLBAR_THICKNESS = 16;
|
||||||
|
@ -294,6 +306,10 @@ const onOpened = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onClosed = () => {
|
||||||
|
emit('closed');
|
||||||
|
};
|
||||||
|
|
||||||
const alignObserver = new ResizeObserver((entries, observer) => {
|
const alignObserver = new ResizeObserver((entries, observer) => {
|
||||||
align();
|
align();
|
||||||
});
|
});
|
||||||
|
@ -311,6 +327,20 @@ onMounted(() => {
|
||||||
align();
|
align();
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
|
watch([showing, () => props.manualShowing], ([showing, manualShowing]) => {
|
||||||
|
if (manualShowing === true || (manualShowing == null && showing === true)) {
|
||||||
|
if (modalRootEl.value != null) {
|
||||||
|
const { release } = focusTrap(modalRootEl.value, props.hasInteractionWithOtherFocusTrappedEls);
|
||||||
|
|
||||||
|
releaseFocusTrap = release;
|
||||||
|
modalRootEl.value.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
releaseFocusTrap?.();
|
||||||
|
focusParent(props.returnFocusTo ?? props.src, true, false);
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
alignObserver.observe(content.value!);
|
alignObserver.observe(content.value!);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="$emit('closed')">
|
<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="emit('closed')" @esc="emit('esc')">
|
||||||
<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown">
|
<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }">
|
||||||
<div ref="headerEl" :class="$style.header">
|
<div ref="headerEl" :class="$style.header">
|
||||||
<button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
|
<button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
|
||||||
<span :class="$style.title">
|
<span :class="$style.title">
|
||||||
|
@ -42,6 +42,7 @@ const emit = defineEmits<{
|
||||||
(event: 'close'): void;
|
(event: 'close'): void;
|
||||||
(event: 'closed'): void;
|
(event: 'closed'): void;
|
||||||
(event: 'ok'): void;
|
(event: 'ok'): void;
|
||||||
|
(event: 'esc'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
@ -58,14 +59,6 @@ const onBgClick = () => {
|
||||||
emit('click');
|
emit('click');
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeydown = (evt) => {
|
|
||||||
if (evt.which === 27) { // Esc
|
|
||||||
evt.preventDefault();
|
|
||||||
evt.stopPropagation();
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ro = new ResizeObserver((entries, observer) => {
|
const ro = new ResizeObserver((entries, observer) => {
|
||||||
if (rootEl.value == null || headerEl.value == null) return;
|
if (rootEl.value == null || headerEl.value == null) return;
|
||||||
bodyWidth.value = rootEl.value.offsetWidth;
|
bodyWidth.value = rootEl.value.offsetWidth;
|
||||||
|
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
ref="rootEl"
|
ref="rootEl"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
|
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
|
||||||
:tabindex="!isDeleted ? '-1' : undefined"
|
:tabindex="isDeleted ? '-1' : '0'"
|
||||||
>
|
>
|
||||||
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||||
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
||||||
|
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
</I18n>
|
</I18n>
|
||||||
<div :class="$style.renoteInfo">
|
<div :class="$style.renoteInfo">
|
||||||
<button ref="renoteTime" :class="$style.renoteTime" class="_button" @click="showRenoteMenu()">
|
<button ref="renoteTime" :class="$style.renoteTime" class="_button" @mousedown.prevent="showRenoteMenu()">
|
||||||
<i class="ti ti-dots" :class="$style.renoteMenu"></i>
|
<i class="ti ti-dots" :class="$style.renoteMenu"></i>
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||||
<MkMediaList :mediaList="appearNote.files"/>
|
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
|
@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
ref="renoteButton"
|
ref="renoteButton"
|
||||||
:class="$style.footerButton"
|
:class="$style.footerButton"
|
||||||
class="_button"
|
class="_button"
|
||||||
@mousedown="renote()"
|
@mousedown.prevent="renote()"
|
||||||
>
|
>
|
||||||
<i class="ti ti-repeat"></i>
|
<i class="ti ti-repeat"></i>
|
||||||
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||||
|
@ -125,10 +125,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
|
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
|
||||||
<i class="ti ti-paperclip"></i>
|
<i class="ti ti-paperclip"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()">
|
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()">
|
||||||
<i class="ti ti-dots"></i>
|
<i class="ti ti-dots"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -174,8 +174,7 @@ import MkPoll from '@/components/MkPoll.vue';
|
||||||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
|
||||||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
|
@ -197,7 +196,10 @@ import { MenuItem } from '@/types/menu.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||||
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
||||||
|
import { host } from '@/config.js';
|
||||||
import { isEnabledUrlPreview } from '@/instance.js';
|
import { isEnabledUrlPreview } from '@/instance.js';
|
||||||
|
import { type Keymap } from '@/scripts/hotkey.js';
|
||||||
|
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -256,6 +258,7 @@ const renoteTime = shallowRef<HTMLElement>();
|
||||||
const reactButton = shallowRef<HTMLElement>();
|
const reactButton = shallowRef<HTMLElement>();
|
||||||
const clipButton = shallowRef<HTMLElement>();
|
const clipButton = shallowRef<HTMLElement>();
|
||||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||||
|
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
|
||||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||||
|
@ -276,6 +279,11 @@ const renoteCollapsed = ref(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||||
|
type: 'lookup',
|
||||||
|
url: `https://${host}/notes/${appearNote.value.id}`,
|
||||||
|
}));
|
||||||
|
|
||||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
/* Overload FunctionにLintが対応していないのでコメントアウト
|
||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
|
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
|
||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
|
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
|
||||||
|
@ -294,15 +302,53 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
|
||||||
}
|
}
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
'r': () => {
|
||||||
'e|a|plus': () => react(true),
|
if (renoteCollapsed.value) return;
|
||||||
'q': () => renote(true),
|
reply();
|
||||||
'up|k|shift+tab': focusBefore,
|
},
|
||||||
'down|j|tab': focusAfter,
|
'e|a|plus': () => {
|
||||||
'esc': blur,
|
if (renoteCollapsed.value) return;
|
||||||
'm|o': () => showMenu(true),
|
react();
|
||||||
's': () => showContent.value !== showContent.value,
|
},
|
||||||
};
|
'q': () => {
|
||||||
|
if (renoteCollapsed.value) return;
|
||||||
|
renote();
|
||||||
|
},
|
||||||
|
'm': () => {
|
||||||
|
if (renoteCollapsed.value) return;
|
||||||
|
showMenu();
|
||||||
|
},
|
||||||
|
'c': () => {
|
||||||
|
if (renoteCollapsed.value) return;
|
||||||
|
if (!defaultStore.state.showClipButtonInNoteFooter) return;
|
||||||
|
clip();
|
||||||
|
},
|
||||||
|
'o': () => {
|
||||||
|
if (renoteCollapsed.value) return;
|
||||||
|
galleryEl.value?.openGallery();
|
||||||
|
},
|
||||||
|
'v|enter': () => {
|
||||||
|
if (renoteCollapsed.value) {
|
||||||
|
renoteCollapsed.value = false;
|
||||||
|
} else if (appearNote.value.cw != null) {
|
||||||
|
showContent.value = !showContent.value;
|
||||||
|
} else if (isLong) {
|
||||||
|
collapsed.value = !collapsed.value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'esc': {
|
||||||
|
allowRepeat: true,
|
||||||
|
callback: () => blur(),
|
||||||
|
},
|
||||||
|
'up|k|shift+tab': {
|
||||||
|
allowRepeat: true,
|
||||||
|
callback: () => focusBefore(),
|
||||||
|
},
|
||||||
|
'down|j|tab': {
|
||||||
|
allowRepeat: true,
|
||||||
|
callback: () => focusAfter(),
|
||||||
|
},
|
||||||
|
} as const satisfies Keymap;
|
||||||
|
|
||||||
provide('react', (reaction: string) => {
|
provide('react', (reaction: string) => {
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
|
@ -371,7 +417,7 @@ if (!props.mock) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renote(viaKeyboard = false) {
|
function renote(viaKeyboard = false) {
|
||||||
pleaseLogin();
|
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
|
const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
|
||||||
|
@ -380,22 +426,21 @@ function renote(viaKeyboard = false) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(): void {
|
||||||
pleaseLogin();
|
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
os.post({
|
os.post({
|
||||||
reply: appearNote.value,
|
reply: appearNote.value,
|
||||||
channel: appearNote.value.channel,
|
channel: appearNote.value.channel,
|
||||||
animation: !viaKeyboard,
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
function react(): void {
|
||||||
pleaseLogin();
|
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
@ -489,18 +534,16 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMenu(viaKeyboard = false): void {
|
function showMenu(): void {
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
||||||
os.popupMenu(menu, menuButton.value, {
|
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
||||||
viaKeyboard,
|
|
||||||
}).then(focus).finally(cleanup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clip() {
|
async function clip(): Promise<void> {
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -508,7 +551,7 @@ async function clip() {
|
||||||
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRenoteMenu(viaKeyboard = false): void {
|
function showRenoteMenu(): void {
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -528,23 +571,19 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMyRenote) {
|
if (isMyRenote) {
|
||||||
pleaseLogin();
|
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||||
os.popupMenu([
|
os.popupMenu([
|
||||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
getUnrenote(),
|
getUnrenote(),
|
||||||
], renoteTime.value, {
|
], renoteTime.value);
|
||||||
viaKeyboard: viaKeyboard,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
os.popupMenu([
|
os.popupMenu([
|
||||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
|
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
|
||||||
($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
|
($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
|
||||||
], renoteTime.value, {
|
], renoteTime.value);
|
||||||
viaKeyboard: viaKeyboard,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -557,11 +596,11 @@ function blur() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusBefore() {
|
function focusBefore() {
|
||||||
focusPrev(rootEl.value ?? null);
|
focusPrev(rootEl.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusAfter() {
|
function focusAfter() {
|
||||||
focusNext(rootEl.value ?? null);
|
focusNext(rootEl.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPromo() {
|
function readPromo() {
|
||||||
|
@ -599,7 +638,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -612,7 +651,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
width: calc(100% - 8px);
|
width: calc(100% - 8px);
|
||||||
height: calc(100% - 8px);
|
height: calc(100% - 8px);
|
||||||
border: dashed 1px var(--focus);
|
border: dashed 2px var(--focus);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
ref="rootEl"
|
ref="rootEl"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
:class="$style.root"
|
:class="$style.root"
|
||||||
|
:tabindex="isDeleted ? '-1' : '0'"
|
||||||
>
|
>
|
||||||
<div v-if="appearNote.reply && appearNote.reply.replyId">
|
<div v-if="appearNote.reply && appearNote.reply.replyId">
|
||||||
<div v-if="!conversationLoaded" style="padding: 16px">
|
<div v-if="!conversationLoaded" style="padding: 16px">
|
||||||
|
@ -31,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</I18n>
|
</I18n>
|
||||||
</span>
|
</span>
|
||||||
<div :class="$style.renoteInfo">
|
<div :class="$style.renoteInfo">
|
||||||
<button ref="renoteTime" class="_button" :class="$style.renoteTime" @click="showRenoteMenu()">
|
<button ref="renoteTime" class="_button" :class="$style.renoteTime" @mousedown.prevent="showRenoteMenu()">
|
||||||
<i v-if="isMyRenote" class="ti ti-dots" style="margin-right: 4px;"></i>
|
<i v-if="isMyRenote" class="ti ti-dots" style="margin-right: 4px;"></i>
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -92,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||||
<MkMediaList :mediaList="appearNote.files"/>
|
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
|
@ -118,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
ref="renoteButton"
|
ref="renoteButton"
|
||||||
class="_button"
|
class="_button"
|
||||||
:class="$style.noteFooterButton"
|
:class="$style.noteFooterButton"
|
||||||
@mousedown="renote()"
|
@mousedown.prevent="renote()"
|
||||||
>
|
>
|
||||||
<i class="ti ti-repeat"></i>
|
<i class="ti ti-repeat"></i>
|
||||||
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||||
|
@ -133,10 +134,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
|
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
|
||||||
<i class="ti ti-paperclip"></i>
|
<i class="ti ti-paperclip"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()">
|
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
|
||||||
<i class="ti ti-dots"></i>
|
<i class="ti ti-dots"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -208,7 +209,7 @@ import MkPoll from '@/components/MkPoll.vue';
|
||||||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import { notePage } from '@/filters/note.js';
|
import { notePage } from '@/filters/note.js';
|
||||||
|
@ -221,6 +222,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { host } from '@/config.js';
|
||||||
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
|
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
|
@ -233,6 +235,7 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
||||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { isEnabledUrlPreview } from '@/instance.js';
|
import { isEnabledUrlPreview } from '@/instance.js';
|
||||||
|
import { type Keymap } from '@/scripts/hotkey.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -280,6 +283,7 @@ const renoteTime = shallowRef<HTMLElement>();
|
||||||
const reactButton = shallowRef<HTMLElement>();
|
const reactButton = shallowRef<HTMLElement>();
|
||||||
const clipButton = shallowRef<HTMLElement>();
|
const clipButton = shallowRef<HTMLElement>();
|
||||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||||
|
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
|
||||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
|
@ -293,14 +297,31 @@ const conversation = ref<Misskey.entities.Note[]>([]);
|
||||||
const replies = ref<Misskey.entities.Note[]>([]);
|
const replies = ref<Misskey.entities.Note[]>([]);
|
||||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
|
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
|
||||||
|
|
||||||
|
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||||
|
type: 'lookup',
|
||||||
|
url: `https://${host}/notes/${appearNote.value.id}`,
|
||||||
|
}));
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
'r': () => reply(),
|
||||||
'e|a|plus': () => react(true),
|
'e|a|plus': () => react(),
|
||||||
'q': () => renote(true),
|
'q': () => renote(),
|
||||||
'esc': blur,
|
'm': () => showMenu(),
|
||||||
'm|o': () => showMenu(true),
|
'c': () => {
|
||||||
's': () => showContent.value !== showContent.value,
|
if (!defaultStore.state.showClipButtonInNoteFooter) return;
|
||||||
};
|
clip();
|
||||||
|
},
|
||||||
|
'o': () => galleryEl.value?.openGallery(),
|
||||||
|
'v|enter': () => {
|
||||||
|
if (appearNote.value.cw != null) {
|
||||||
|
showContent.value = !showContent.value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'esc': {
|
||||||
|
allowRepeat: true,
|
||||||
|
callback: () => blur(),
|
||||||
|
},
|
||||||
|
} as const satisfies Keymap;
|
||||||
|
|
||||||
provide('react', (reaction: string) => {
|
provide('react', (reaction: string) => {
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
|
@ -380,30 +401,27 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renote(viaKeyboard = false) {
|
function renote() {
|
||||||
pleaseLogin();
|
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
|
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
|
||||||
os.popupMenu(menu, renoteButton.value, {
|
os.popupMenu(menu, renoteButton.value);
|
||||||
viaKeyboard,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(): void {
|
||||||
pleaseLogin();
|
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
os.post({
|
os.post({
|
||||||
reply: appearNote.value,
|
reply: appearNote.value,
|
||||||
channel: appearNote.value.channel,
|
channel: appearNote.value.channel,
|
||||||
animation: !viaKeyboard,
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
function react(): void {
|
||||||
pleaseLogin();
|
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
@ -412,7 +430,7 @@ function react(viaKeyboard = false): void {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.value.id,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
});
|
});
|
||||||
const el = reactButton.value as HTMLElement | null | undefined;
|
const el = reactButton.value;
|
||||||
if (el) {
|
if (el) {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const x = rect.left + (el.offsetWidth / 2);
|
const x = rect.left + (el.offsetWidth / 2);
|
||||||
|
@ -476,20 +494,18 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMenu(viaKeyboard = false): void {
|
function showMenu(): void {
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
|
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
|
||||||
os.popupMenu(menu, menuButton.value, {
|
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
||||||
viaKeyboard,
|
|
||||||
}).then(focus).finally(cleanup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clip() {
|
async function clip(): Promise<void> {
|
||||||
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
|
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRenoteMenu(viaKeyboard = false): void {
|
function showRenoteMenu(): void {
|
||||||
if (!isMyRenote) return;
|
if (!isMyRenote) return;
|
||||||
pleaseLogin();
|
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: i18n.ts.unrenote,
|
text: i18n.ts.unrenote,
|
||||||
icon: 'ti ti-trash',
|
icon: 'ti ti-trash',
|
||||||
|
@ -500,9 +516,7 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||||
});
|
});
|
||||||
isDeleted.value = true;
|
isDeleted.value = true;
|
||||||
},
|
},
|
||||||
}], renoteTime.value, {
|
}], renoteTime.value);
|
||||||
viaKeyboard: viaKeyboard,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
|
@ -544,6 +558,28 @@ function loadConversation() {
|
||||||
transition: box-shadow 0.1s ease;
|
transition: box-shadow 0.1s ease;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
height: calc(100% - 8px);
|
||||||
|
border: dashed 2px var(--focus);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.replyTo {
|
.replyTo {
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<MkAvatar :class="$style.avatar" :user="user" link preview/>
|
<MkAvatar :class="$style.avatar" :user="user"/>
|
||||||
<div :class="$style.main">
|
<div :class="$style.main">
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<MkUserName :user="user" :nowrap="true"/>
|
<MkUserName :user="user" :nowrap="true"/>
|
||||||
|
|
|
@ -343,7 +343,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
|
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj">
|
||||||
<div v-if="page.eyeCatchingImage" class="thumbnail">
|
<div v-if="page.eyeCatchingImage" class="thumbnail">
|
||||||
<MediaImage
|
<MediaImage
|
||||||
:image="page.eyeCatchingImage"
|
:image="page.eyeCatchingImage"
|
||||||
|
@ -50,12 +50,29 @@ const props = defineProps<{
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.vhpxefrj {
|
.vhpxefrj {
|
||||||
display: block;
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: inset 0 0 0 2px var(--focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .thumbnail {
|
> .thumbnail {
|
||||||
& + article {
|
& + article {
|
||||||
border-radius: 0 0 var(--radius) var(--radius);
|
border-radius: 0 0 var(--radius) var(--radius);
|
||||||
|
|
|
@ -34,7 +34,9 @@ import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { host } from '@/config.js';
|
||||||
import { useInterval } from '@/scripts/use-interval.js';
|
import { useInterval } from '@/scripts/use-interval.js';
|
||||||
|
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
noteId: string;
|
noteId: string;
|
||||||
|
@ -60,6 +62,11 @@ const timer = computed(() => i18n.tsx._poll[
|
||||||
|
|
||||||
const showResult = ref(props.readOnly || isVoted.value);
|
const showResult = ref(props.readOnly || isVoted.value);
|
||||||
|
|
||||||
|
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||||
|
type: 'lookup',
|
||||||
|
url: `https://${host}/notes/${props.noteId}`,
|
||||||
|
}));
|
||||||
|
|
||||||
// 期限付きアンケート
|
// 期限付きアンケート
|
||||||
if (props.poll.expiresAt) {
|
if (props.poll.expiresAt) {
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
|
@ -76,7 +83,7 @@ if (props.poll.expiresAt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const vote = async (id) => {
|
const vote = async (id) => {
|
||||||
pleaseLogin();
|
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||||
|
|
||||||
if (props.readOnly || closed.value || isVoted.value) return;
|
if (props.readOnly || closed.value || isVoted.value) return;
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" @click="click" @close="onModalClose" @closed="onModalClosed">
|
<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" :returnFocusTo="returnFocusTo" @click="click" @close="onModalClose" @closed="onModalClosed">
|
||||||
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/>
|
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :returnFocusTo="returnFocusTo" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -19,8 +19,8 @@ defineProps<{
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
align?: 'center' | string;
|
align?: 'center' | string;
|
||||||
width?: number;
|
width?: number;
|
||||||
viaKeyboard?: boolean;
|
|
||||||
src?: any;
|
src?: any;
|
||||||
|
returnFocusTo?: HTMLElement | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
|
@ -570,6 +570,7 @@ function clear() {
|
||||||
|
|
||||||
function onKeydown(ev: KeyboardEvent) {
|
function onKeydown(ev: KeyboardEvent) {
|
||||||
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
|
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
|
||||||
|
|
||||||
if (ev.key === 'Escape') emit('esc');
|
if (ev.key === 'Escape') emit('esc');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1083,6 +1084,15 @@ defineExpose({
|
||||||
margin: 12px 12px 12px 6px;
|
margin: 12px 12px 12px 6px;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.submitInner {
|
||||||
|
outline: 2px solid var(--fgOnAccent);
|
||||||
|
outline-offset: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()">
|
<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()" @esc="modal?.close()">
|
||||||
<MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/>
|
<MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
|
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
|
||||||
:aria-checked="checked"
|
:aria-checked="checked"
|
||||||
:aria-disabled="disabled"
|
:aria-disabled="disabled"
|
||||||
|
role="checkbox"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
@ -69,6 +70,11 @@ function toggle(): void {
|
||||||
border-color: var(--inputBorderHover) !important;
|
border-color: var(--inputBorderHover) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--focus);
|
||||||
|
}
|
||||||
|
|
||||||
&.checked {
|
&.checked {
|
||||||
background-color: var(--accentedBg) !important;
|
background-color: var(--accentedBg) !important;
|
||||||
border-color: var(--accentedBg) !important;
|
border-color: var(--accentedBg) !important;
|
||||||
|
@ -78,7 +84,7 @@ function toggle(): void {
|
||||||
> .button {
|
> .button {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
background-color: var(--accent);
|
background-color: var(--accent);
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -104,7 +110,7 @@ function toggle(): void {
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
transition: inherit;
|
transition: inherit;
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -6,20 +6,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div :class="$style.label" @click="focus"><slot name="label"></slot></div>
|
<div :class="$style.label" @click="focus"><slot name="label"></slot></div>
|
||||||
<div ref="container" :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]" @mousedown.prevent="show">
|
<div
|
||||||
|
ref="container"
|
||||||
|
tabindex="0"
|
||||||
|
:class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused || opening }]"
|
||||||
|
@focus="focused = true"
|
||||||
|
@blur="focused = false"
|
||||||
|
@mousedown.prevent="show"
|
||||||
|
@keydown.space.enter="show"
|
||||||
|
>
|
||||||
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
|
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
|
||||||
<select
|
<select
|
||||||
ref="inputEl"
|
ref="inputEl"
|
||||||
v-model="v"
|
v-model="v"
|
||||||
v-adaptive-border
|
v-adaptive-border
|
||||||
|
tabindex="-1"
|
||||||
:class="$style.inputCore"
|
:class="$style.inputCore"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:required="required"
|
:required="required"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@focus="focused = true"
|
|
||||||
@blur="focused = false"
|
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
|
@mousedown.prevent="() => {}"
|
||||||
|
@keydown.prevent="() => {}"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</select>
|
</select>
|
||||||
|
@ -75,7 +84,7 @@ const height =
|
||||||
props.large ? 39 :
|
props.large ? 39 :
|
||||||
36;
|
36;
|
||||||
|
|
||||||
const focus = () => inputEl.value?.focus();
|
const focus = () => container.value?.focus();
|
||||||
const onInput = (ev) => {
|
const onInput = (ev) => {
|
||||||
changed.value = true;
|
changed.value = true;
|
||||||
};
|
};
|
||||||
|
@ -126,7 +135,9 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
focused.value = true;
|
if (opening.value) return;
|
||||||
|
focus();
|
||||||
|
|
||||||
opening.value = true;
|
opening.value = true;
|
||||||
|
|
||||||
const menu: MenuItem[] = [];
|
const menu: MenuItem[] = [];
|
||||||
|
@ -173,8 +184,6 @@ function show() {
|
||||||
onClosing: () => {
|
onClosing: () => {
|
||||||
opening.value = false;
|
opening.value = false;
|
||||||
},
|
},
|
||||||
}).then(() => {
|
|
||||||
focused.value = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -225,6 +234,10 @@ function show() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
> .inputCore {
|
> .inputCore {
|
||||||
border-color: var(--inputBorderHover) !important;
|
border-color: var(--inputBorderHover) !important;
|
||||||
|
|
|
@ -6,10 +6,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
|
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
|
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
|
||||||
<MkInfo v-if="message">
|
<MkInfo v-if="message">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</MkInfo>
|
</MkInfo>
|
||||||
|
<div v-if="openOnRemote" class="_gaps_m">
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)">
|
||||||
|
{{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
|
||||||
|
</MkButton>
|
||||||
|
<button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)">
|
||||||
|
{{ i18n.ts.specifyServerHost }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.orHr">
|
||||||
|
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="!totpLogin" class="normal-signin _gaps_m">
|
<div v-if="!totpLogin" class="normal-signin _gaps_m">
|
||||||
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||||
<template #prefix>@</template>
|
<template #prefix>@</template>
|
||||||
|
@ -28,8 +41,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
{{ i18n.ts.retry }}
|
{{ i18n.ts.retry }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="user && user.securityKeys" class="or-hr">
|
<div v-if="user && user.securityKeys" :class="$style.orHr">
|
||||||
<p class="or-msg">{{ i18n.ts.or }}</p>
|
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="twofa-group totp-group _gaps">
|
<div class="twofa-group totp-group _gaps">
|
||||||
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
|
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
|
||||||
|
@ -53,6 +66,7 @@ import { defineAsyncComponent, ref } from 'vue';
|
||||||
import { toUnicode } from 'punycode/';
|
import { toUnicode } from 'punycode/';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
|
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
|
||||||
|
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||||
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
@ -60,6 +74,7 @@ import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { host as configHost } from '@/config.js';
|
import { host as configHost } from '@/config.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
|
import { query, extractDomain } from '@/scripts/url.js';
|
||||||
import { login } from '@/account.js';
|
import { login } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
@ -78,22 +93,16 @@ const emit = defineEmits<{
|
||||||
(ev: 'login', v: any): void;
|
(ev: 'login', v: any): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = withDefaults(defineProps<{
|
||||||
withAvatar: {
|
withAvatar?: boolean;
|
||||||
type: Boolean,
|
autoSet?: boolean;
|
||||||
required: false,
|
message?: string,
|
||||||
default: true,
|
openOnRemote?: OpenOnRemoteOptions,
|
||||||
},
|
}>(), {
|
||||||
autoSet: {
|
withAvatar: true,
|
||||||
type: Boolean,
|
autoSet: false,
|
||||||
required: false,
|
message: '',
|
||||||
default: false,
|
openOnRemote: undefined,
|
||||||
},
|
|
||||||
message: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function onUsernameChange(): void {
|
function onUsernameChange(): void {
|
||||||
|
@ -222,6 +231,62 @@ function resetPassword(): void {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
|
||||||
|
switch (options.type) {
|
||||||
|
case 'web':
|
||||||
|
case 'lookup': {
|
||||||
|
let _path: string;
|
||||||
|
|
||||||
|
if (options.type === 'lookup') {
|
||||||
|
// TODO: v2024.7.0以降が浸透してきたら正式なURLに変更する▼
|
||||||
|
// _path = `/lookup?uri=${encodeURIComponent(_path)}`;
|
||||||
|
_path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`;
|
||||||
|
} else {
|
||||||
|
_path = options.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetHost) {
|
||||||
|
window.open(`https://${targetHost}${_path}`, '_blank', 'noopener');
|
||||||
|
} else {
|
||||||
|
window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'share': {
|
||||||
|
const params = query(options.params);
|
||||||
|
if (targetHost) {
|
||||||
|
window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener');
|
||||||
|
} else {
|
||||||
|
window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> {
|
||||||
|
const { canceled, result: hostTemp } = await os.inputText({
|
||||||
|
title: i18n.ts.inputHostName,
|
||||||
|
placeholder: 'misskey.example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
let targetHost: string | null = hostTemp;
|
||||||
|
|
||||||
|
// ドメイン部分だけを取り出す
|
||||||
|
targetHost = extractDomain(targetHost);
|
||||||
|
if (targetHost == null) {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
title: i18n.ts.invalidValue,
|
||||||
|
text: i18n.ts.tryAgain,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openRemote(options, targetHost);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -234,4 +299,36 @@ function resetPassword(): void {
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.instanceManualSelectButton {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
opacity: .7;
|
||||||
|
font-size: .8em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.orHr {
|
||||||
|
position: relative;
|
||||||
|
margin: .4em auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orMsg {
|
||||||
|
position: absolute;
|
||||||
|
top: -.6em;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 1em;
|
||||||
|
background: var(--panel);
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: var(--fgOnPanel);
|
||||||
|
margin: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,21 +6,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<MkModalWindow
|
<MkModalWindow
|
||||||
ref="dialog"
|
ref="dialog"
|
||||||
:width="370"
|
:width="400"
|
||||||
:height="400"
|
:height="430"
|
||||||
@close="onClose"
|
@close="onClose"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts.login }}</template>
|
<template #header>{{ i18n.ts.login }}</template>
|
||||||
|
|
||||||
<MkSpacer :marginMin="20" :marginMax="28">
|
<MkSpacer :marginMin="20" :marginMax="28">
|
||||||
<MkSignin :autoSet="autoSet" :message="message" @login="onLogin"/>
|
<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</MkModalWindow>
|
</MkModalWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { shallowRef } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
|
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||||
import MkSignin from '@/components/MkSignin.vue';
|
import MkSignin from '@/components/MkSignin.vue';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -28,9 +29,11 @@ import { i18n } from '@/i18n.js';
|
||||||
withDefaults(defineProps<{
|
withDefaults(defineProps<{
|
||||||
autoSet?: boolean;
|
autoSet?: boolean;
|
||||||
message?: string,
|
message?: string,
|
||||||
|
openOnRemote?: OpenOnRemoteOptions,
|
||||||
}>(), {
|
}>(), {
|
||||||
autoSet: false,
|
autoSet: false,
|
||||||
message: '',
|
message: '',
|
||||||
|
openOnRemote: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
|
@ -10,15 +10,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<div class="items">
|
<div class="items">
|
||||||
<template v-for="(item, i) in group.items">
|
<template v-for="(item, i) in group.items">
|
||||||
<a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
<a v-if="item.type === 'a'" :href="item.href" :target="item.target" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||||
<span class="text">{{ item.text }}</span>
|
<span class="text">{{ item.text }}</span>
|
||||||
</a>
|
</a>
|
||||||
<button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
|
<button v-else-if="item.type === 'button'" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
|
||||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||||
<span class="text">{{ item.text }}</span>
|
<span class="text">{{ item.text }}</span>
|
||||||
</button>
|
</button>
|
||||||
<MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
<MkA v-else :to="item.to" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||||
<span class="text">{{ item.text }}</span>
|
<span class="text">{{ item.text }}</span>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
@ -67,6 +67,10 @@ defineProps<{
|
||||||
background: var(--panelHighlight);
|
background: var(--panelHighlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background: var(--accentedBg);
|
background: var(--accentedBg);
|
||||||
|
|
|
@ -10,9 +10,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:class="$style.input"
|
:class="$style.input"
|
||||||
@keydown.enter="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<XButton :checked="checked" :disabled="disabled" @toggle="toggle"/>
|
<XButton :class="$style.toggle" :checked="checked" :disabled="disabled" @toggle="toggle"/>
|
||||||
<span v-if="!noBody" :class="$style.body">
|
<span v-if="!noBody" :class="$style.body">
|
||||||
<!-- TODO: 無名slotの方は廃止 -->
|
<!-- TODO: 無名slotの方は廃止 -->
|
||||||
<span :class="$style.label">
|
<span :class="$style.label">
|
||||||
|
@ -76,7 +76,13 @@ const toggle = () => {
|
||||||
height: 0;
|
height: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
|
&:focus-visible ~ .toggle {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
|
|
@ -105,7 +105,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(100% - 38px);
|
width: calc(100% - 38px);
|
||||||
|
|
|
@ -115,7 +115,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(100% - 38px);
|
width: calc(100% - 38px);
|
||||||
|
|
|
@ -55,7 +55,7 @@ import { i18n } from '@/i18n.js';
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(100% - 38px);
|
width: calc(100% - 38px);
|
||||||
|
|
|
@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span>
|
<p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/>
|
<MkFollowButton v-if="user.id != $i?.id" :class="$style.follow" :user="user" mini/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')">
|
<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()">
|
||||||
<div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }">
|
<div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }">
|
||||||
<div :class="[$style.label, $style.item]">
|
<div :class="[$style.label, $style.item]">
|
||||||
{{ i18n.ts.visibility }}
|
{{ i18n.ts.visibility }}
|
||||||
|
|
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div class="_gaps_s" :class="$style.mainActions">
|
<div class="_gaps_s" :class="$style.mainActions">
|
||||||
<MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
|
<MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
|
||||||
<MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton>
|
<MkButton :class="$style.mainAction" full rounded link to="https://misskey-hub.net/servers/">{{ i18n.ts.exploreOtherServers }}</MkButton>
|
||||||
<MkButton :class="$style.mainAction" full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton>
|
<MkButton :class="$style.mainAction" full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -65,7 +65,8 @@ import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import MkNumber from '@/components/MkNumber.vue';
|
import MkNumber from '@/components/MkNumber.vue';
|
||||||
import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue';
|
import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue';
|
||||||
import { openInstanceMenu } from '@/ui/_common_/common';
|
import { openInstanceMenu } from '@/ui/_common_/common.js';
|
||||||
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
|
|
||||||
const stats = ref<Misskey.entities.StatsResponse | null>(null);
|
const stats = ref<Misskey.entities.StatsResponse | null>(null);
|
||||||
|
|
||||||
|
@ -89,13 +90,9 @@ function signup() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMenu(ev) {
|
function showMenu(ev: MouseEvent) {
|
||||||
openInstanceMenu(ev);
|
openInstanceMenu(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exploreOtherServers() {
|
|
||||||
window.open('https://misskey-hub.net/servers/', '_blank', 'noopener');
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -8,7 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div ref="headerEl">
|
<div ref="headerEl">
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div ref="bodyEl" :data-sticky-container-header-height="headerHeight">
|
<div
|
||||||
|
ref="bodyEl"
|
||||||
|
:data-sticky-container-header-height="headerHeight"
|
||||||
|
:data-sticky-container-footer-height="footerHeight"
|
||||||
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<div ref="footerEl">
|
<div ref="footerEl">
|
||||||
|
|
|
@ -60,7 +60,7 @@ function onChange({ resolved, key: newKey }) {
|
||||||
if (current == null || 'redirect' in current.route) return;
|
if (current == null || 'redirect' in current.route) return;
|
||||||
currentPageComponent.value = current.route.component;
|
currentPageComponent.value = current.route.component;
|
||||||
currentPageProps.value = current.props;
|
currentPageProps.value = current.props;
|
||||||
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
|
key.value = newKey + JSON.stringify(Object.fromEntries(current.props));
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// ページ遷移完了後に再びキャッシュを有効化
|
// ページ遷移完了後に再びキャッシュを有効化
|
||||||
|
|
|
@ -87,6 +87,7 @@ export const ROLE_POLICIES = [
|
||||||
'canHideAds',
|
'canHideAds',
|
||||||
'driveCapacityMb',
|
'driveCapacityMb',
|
||||||
'alwaysMarkNsfw',
|
'alwaysMarkNsfw',
|
||||||
|
'canUpdateBioMedia',
|
||||||
'pinLimit',
|
'pinLimit',
|
||||||
'antennaLimit',
|
'antennaLimit',
|
||||||
'wordMuteLimit',
|
'wordMuteLimit',
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Directive } from 'vue';
|
import { Directive } from 'vue';
|
||||||
import { makeHotkey } from '../scripts/hotkey.js';
|
import { makeHotkey } from '@/scripts/hotkey.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mounted(el, binding) {
|
mounted(el, binding) {
|
||||||
|
@ -13,9 +13,9 @@ export default {
|
||||||
el._keyHandler = makeHotkey(binding.value);
|
el._keyHandler = makeHotkey(binding.value);
|
||||||
|
|
||||||
if (el._hotkey_global) {
|
if (el._hotkey_global) {
|
||||||
document.addEventListener('keydown', el._keyHandler);
|
document.addEventListener('keydown', el._keyHandler, { passive: false });
|
||||||
} else {
|
} else {
|
||||||
el.addEventListener('keydown', el._keyHandler);
|
el.addEventListener('keydown', el._keyHandler, { passive: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue