Compare commits
57 Commits
2023.11.0-
...
notificati
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5ec1d96048 | ||
![]() |
441c0ca465 | ||
![]() |
29b06994a2 | ||
![]() |
314fb4bfd9 | ||
![]() |
ce5ff70cb3 | ||
![]() |
6a73f7c108 | ||
![]() |
c54baf873b | ||
![]() |
e88dbad3cf | ||
![]() |
5772de2a62 | ||
![]() |
821633f878 | ||
![]() |
9b073e5fe6 | ||
![]() |
77db652bff | ||
![]() |
e632a84431 | ||
![]() |
7ed2a5fc1b | ||
![]() |
5fb6847419 | ||
![]() |
e85b8217c0 | ||
![]() |
d6fe897923 | ||
![]() |
bf01c1ee64 | ||
![]() |
7d3721dded | ||
![]() |
735f22c1c5 | ||
![]() |
cf026e4c72 | ||
![]() |
e2f34e3db6 | ||
![]() |
7c692283ad | ||
![]() |
e6e5bf1da4 | ||
![]() |
a35fe29ef4 | ||
![]() |
56c5da97e6 | ||
![]() |
af779ebff9 | ||
![]() |
4eab3c07fd | ||
![]() |
359f3d5ef5 | ||
![]() |
d45b2dd3a7 | ||
![]() |
b4dd61a016 | ||
![]() |
4f180ad45c | ||
![]() |
52dbab56a4 | ||
![]() |
7015cc937b | ||
![]() |
50b16e36c7 | ||
![]() |
e512f8c56d | ||
![]() |
183e5cef8b | ||
![]() |
38c163d67c | ||
![]() |
20f70f1c39 | ||
![]() |
c239058624 | ||
![]() |
117db08880 | ||
![]() |
2de4d3329d | ||
![]() |
8f01757a7f | ||
![]() |
d9cfea8b10 | ||
![]() |
cb1449be09 | ||
![]() |
9ad48dae04 | ||
![]() |
59cc101752 | ||
![]() |
aefc941df3 | ||
![]() |
2da55f70a7 | ||
![]() |
0fc36d11d7 | ||
![]() |
7436e0da18 | ||
![]() |
a161a9c1e7 | ||
![]() |
1a8243f1ca | ||
![]() |
feedad7d8b | ||
![]() |
b627978d00 | ||
![]() |
2a61a0c026 | ||
![]() |
5887c5da6c |
60
.github/ISSUE_TEMPLATE/01_bug-report.md
vendored
60
.github/ISSUE_TEMPLATE/01_bug-report.md
vendored
@@ -1,60 +0,0 @@
|
|||||||
---
|
|
||||||
name: 🐛 Bug Report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: ⚠️bug?
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thanks for reporting!
|
|
||||||
First, in order to avoid duplicate Issues, please search to see if the problem you found has already been reported.
|
|
||||||
Also, If you are NOT owner/admin of server, PLEASE DONT REPORT SERVER SPECIFIC ISSUES TO HERE! (e.g. feature XXX is not working in misskey.example) Please try with another misskey servers, and if your issue is only reproducible with specific server, contact your server's owner/admin first.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## 💡 Summary
|
|
||||||
|
|
||||||
<!-- Tell us what the bug is -->
|
|
||||||
|
|
||||||
## 🥰 Expected Behavior
|
|
||||||
|
|
||||||
<!--- Tell us what should happen -->
|
|
||||||
|
|
||||||
## 🤬 Actual Behavior
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Tell us what happens instead of the expected behavior.
|
|
||||||
Please include errors from the developer console and/or server log files if you have access to them.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## 📝 Steps to Reproduce
|
|
||||||
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
|
|
||||||
## 📌 Environment
|
|
||||||
|
|
||||||
<!-- Tell us where on the platform it happens -->
|
|
||||||
<!-- DO NOT WRITE "latest". Please provide the specific version. -->
|
|
||||||
|
|
||||||
### 💻 Frontend
|
|
||||||
* Model and OS of the device(s):
|
|
||||||
<!-- Example: MacBook Pro (14inch, 2021), macOS Ventura 13.4 -->
|
|
||||||
* Browser:
|
|
||||||
<!-- Example: Chrome 113.0.5672.126 -->
|
|
||||||
* Server URL:
|
|
||||||
<!-- Example: misskey.io -->
|
|
||||||
* Misskey:
|
|
||||||
13.x.x
|
|
||||||
|
|
||||||
### 🛰 Backend (for server admin)
|
|
||||||
<!-- If you are using a managed service, put that after the version. -->
|
|
||||||
|
|
||||||
* Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment -->
|
|
||||||
* Misskey: 13.x.x
|
|
||||||
* Node: 20.x.x
|
|
||||||
* PostgreSQL: 15.x.x
|
|
||||||
* Redis: 7.x.x
|
|
||||||
* OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 -->
|
|
91
.github/ISSUE_TEMPLATE/01_bug-report.yml
vendored
Normal file
91
.github/ISSUE_TEMPLATE/01_bug-report.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
name: 🐛 Bug Report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
labels: ["⚠️bug?"]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for reporting!
|
||||||
|
First, in order to avoid duplicate Issues, please search to see if the problem you found has already been reported.
|
||||||
|
Also, If you are NOT owner/admin of server, PLEASE DONT REPORT SERVER SPECIFIC ISSUES TO HERE! (e.g. feature XXX is not working in misskey.example) Please try with another misskey servers, and if your issue is only reproducible with specific server, contact your server's owner/admin first.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 💡 Summary
|
||||||
|
description: Tell us what the bug is
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 🥰 Expected Behavior
|
||||||
|
description: Tell us what should happen
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 🤬 Actual Behavior
|
||||||
|
description: |
|
||||||
|
Tell us what happens instead of the expected behavior.
|
||||||
|
Please include errors from the developer console and/or server log files if you have access to them.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 📝 Steps to Reproduce
|
||||||
|
placeholder: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 💻 Frontend Environment
|
||||||
|
description: |
|
||||||
|
Tell us where on the platform it happens
|
||||||
|
DO NOT WRITE "latest". Please provide the specific version.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
* Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4
|
||||||
|
* Browser: Chrome 113.0.5672.126
|
||||||
|
* Server URL: misskey.io
|
||||||
|
* Misskey: 13.x.x
|
||||||
|
value: |
|
||||||
|
* Model and OS of the device(s):
|
||||||
|
* Browser:
|
||||||
|
* Server URL:
|
||||||
|
* Misskey:
|
||||||
|
render: markdown
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 🛰 Backend Environment (for server admin)
|
||||||
|
description: |
|
||||||
|
Tell us where on the platform it happens
|
||||||
|
DO NOT WRITE "latest". Please provide the specific version.
|
||||||
|
If you are using a managed service, put that after the version.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
* Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment
|
||||||
|
* Misskey: 13.x.x
|
||||||
|
* Node: 20.x.x
|
||||||
|
* PostgreSQL: 15.x.x
|
||||||
|
* Redis: 7.x.x
|
||||||
|
* OS and Architecture: Ubuntu 22.04.2 LTS aarch64
|
||||||
|
value: |
|
||||||
|
* Installation Method or Hosting Service:
|
||||||
|
* Misskey:
|
||||||
|
* Node:
|
||||||
|
* PostgreSQL:
|
||||||
|
* Redis:
|
||||||
|
* OS and Architecture:
|
||||||
|
render: markdown
|
||||||
|
validations:
|
||||||
|
required: false
|
12
.github/ISSUE_TEMPLATE/02_feature-request.md
vendored
12
.github/ISSUE_TEMPLATE/02_feature-request.md
vendored
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
name: ✨ Feature Request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: ✨Feature
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
<!-- Tell us what the suggestion is -->
|
|
11
.github/ISSUE_TEMPLATE/02_feature-request.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/02_feature-request.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
name: ✨ Feature Request
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
labels: ["✨Feature"]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Summary
|
||||||
|
description: Tell us what the suggestion is
|
||||||
|
validations:
|
||||||
|
required: true
|
2
.github/workflows/api-misskey-js.yml
vendored
2
.github/workflows/api-misskey-js.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
4
.github/workflows/get-api-diff.yml
vendored
4
.github/workflows/get-api-diff.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
|||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -126,7 +126,7 @@ jobs:
|
|||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
- uses: actions/setup-node@v3.8.1
|
- uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 7
|
version: 7
|
||||||
run_install: false
|
run_install: false
|
||||||
- uses: actions/setup-node@v3.8.1
|
- uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 7
|
version: 7
|
||||||
run_install: false
|
run_install: false
|
||||||
- uses: actions/setup-node@v3.8.1
|
- uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
2
.github/workflows/test-backend.yml
vendored
2
.github/workflows/test-backend.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
4
.github/workflows/test-frontend.yml
vendored
4
.github/workflows/test-frontend.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
version: 7
|
version: 7
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
2
.github/workflows/test-misskey-js.yml
vendored
2
.github/workflows/test-misskey-js.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
|
|
||||||
- name: Setup Node.js ${{ matrix.node-version }}
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
2
.github/workflows/test-production.yml
vendored
2
.github/workflows/test-production.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
29
CHANGELOG.md
29
CHANGELOG.md
@@ -16,30 +16,57 @@
|
|||||||
|
|
||||||
### General
|
### General
|
||||||
- Feat: アイコンデコレーション機能
|
- Feat: アイコンデコレーション機能
|
||||||
|
- サーバーで用意された画像をアイコンに重ねることができます
|
||||||
|
- 画像のテンプレートはこちらです: https://misskey-hub.net/avatar-decoration-template.png
|
||||||
|
- 最大でも黄色いエリア内にデコレーションを収めることを推奨します。
|
||||||
|
- 画像は512x512pxを推奨します。
|
||||||
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
|
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
|
||||||
|
- Enhance: 未読の通知数を表示できるように
|
||||||
|
- Enhance: ローカリゼーションの更新
|
||||||
|
- Enhance: 依存関係の更新
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました
|
- Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました
|
||||||
- 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください
|
- 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください
|
||||||
https://misskey-hub.net/docs/advanced/publish-on-your-website.html
|
https://misskey-hub.net/docs/advanced/publish-on-your-website.html
|
||||||
|
- Enhance: スワイプしてタイムラインを再読込できるように
|
||||||
|
- PCの場合は右上のボタンからでも再読込できます
|
||||||
|
- Enhance: タイムラインの自動更新を無効にできるように
|
||||||
|
- Enhance: 通知をグルーピングして表示するオプション(オプトアウト)
|
||||||
|
- Enhance: コードのシンタックスハイライトエンジンをShikiに変更
|
||||||
|
- AiScriptのシンタックスハイライトに対応
|
||||||
|
- MFMでAiScriptをハイライトする場合、コードブロックの開始部分を ` ```is ` もしくは ` ```aiscript ` としてください
|
||||||
- Enhance: データセーバー有効時はアニメーション付きのアバター画像が停止するように
|
- Enhance: データセーバー有効時はアニメーション付きのアバター画像が停止するように
|
||||||
- Enhance: プラグインを削除した際には、使用されていたアクセストークンも同時に削除されるようになりました
|
- Enhance: プラグインを削除した際には、使用されていたアクセストークンも同時に削除されるようになりました
|
||||||
- Enhance: プラグインで`Plugin:register_note_view_interruptor`を用いてnoteの代わりにnullを返却することでノートを非表示にできるようになりました
|
- Enhance: プラグインで`Plugin:register_note_view_interruptor`を用いてnoteの代わりにnullを返却することでノートを非表示にできるようになりました
|
||||||
- Enhance: AiScript関数`Mk:nyaize()`が追加されました
|
- Enhance: AiScript関数`Mk:nyaize()`が追加されました
|
||||||
|
- Enhance: 情報→ツール はナビゲーションバーにツールとして独立した項目になりました
|
||||||
|
- Enhance: その他細かなブラッシュアップ
|
||||||
- Fix: 投稿フォームでのユーザー変更がプレビューに反映されない問題を修正
|
- Fix: 投稿フォームでのユーザー変更がプレビューに反映されない問題を修正
|
||||||
- Fix: ユーザーページの ノート > ファイル付き タブにリプライが表示されてしまう
|
- Fix: ユーザーページの ノート > ファイル付き タブにリプライが表示されてしまう
|
||||||
- Fix: 「検索」MFMにおいて一部の検索キーワードが正しく認識されない問題を修正
|
- Fix: 「検索」MFMにおいて一部の検索キーワードが正しく認識されない問題を修正
|
||||||
- Fix: 一部の言語でMisskey Webがクラッシュする問題を修正
|
- Fix: 一部の言語でMisskey Webがクラッシュする問題を修正
|
||||||
|
- Fix: チャンネルの作成・更新時に失敗した場合何も表示されない問題を修正 #11983
|
||||||
|
- Fix: 個人カードのemojiがバッテリーになっている問題を修正
|
||||||
|
- Fix: 標準テーマと同じIDを使用してインストールできてしまう問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Enhance: RedisへのTLのキャッシュをオフにできるように
|
- Enhance: RedisへのTLのキャッシュ(FTT)をオフにできるように
|
||||||
- Enhance: フォローしているチャンネルをフォロー解除した時(またはその逆)、タイムラインに反映される間隔を改善
|
- Enhance: フォローしているチャンネルをフォロー解除した時(またはその逆)、タイムラインに反映される間隔を改善
|
||||||
|
- Enhance: プロフィールの自己紹介欄のMFMが連合するようになりました
|
||||||
|
- 相手がMisskey v2023.11.0以降である必要があります
|
||||||
|
- Enhance: チャンネル取得時のパフォーマンスを向上
|
||||||
- Fix: リストTLに自分のフォロワー限定投稿が含まれない問題を修正
|
- Fix: リストTLに自分のフォロワー限定投稿が含まれない問題を修正
|
||||||
- Fix: ローカルタイムラインに投稿者自身の投稿への返信が含まれない問題を修正
|
- Fix: ローカルタイムラインに投稿者自身の投稿への返信が含まれない問題を修正
|
||||||
- Fix: 自分のフォローしているユーザーの自分のフォローしていないユーザーの visibility: followers な投稿への返信がストリーミングで流れてくる問題を修正
|
- Fix: 自分のフォローしているユーザーの自分のフォローしていないユーザーの visibility: followers な投稿への返信がストリーミングで流れてくる問題を修正
|
||||||
- Fix: RedisへのTLキャッシュが有効の場合にHTL/LTL/STLが空になることがある問題を修正
|
- Fix: RedisへのTLキャッシュが有効の場合にHTL/LTL/STLが空になることがある問題を修正
|
||||||
- Fix: STLでフォローしていないチャンネルが取得される問題を修正
|
- Fix: STLでフォローしていないチャンネルが取得される問題を修正
|
||||||
- Fix: `hashtags/trend`にてRedisからトレンドの情報が取得できない際にInternal Server Errorになる問題を修正
|
- Fix: `hashtags/trend`にてRedisからトレンドの情報が取得できない際にInternal Server Errorになる問題を修正
|
||||||
|
- Fix: HTLをリロードまたは遡行したとき、フォローしているチャンネルのノートが含まれない問題を修正 #11765 #12181
|
||||||
|
- Fix: リノートをリノートできるのを修正
|
||||||
|
- Fix: アクセストークンを削除すると、通知が取得できなくなる場合がある問題を修正
|
||||||
|
- Fix: 自身の宛先なしダイレクト投稿がストリーミングで流れてこない問題を修正
|
||||||
|
- Fix: サーバーサイドからのテスト通知を正しく行えるように修正
|
||||||
|
|
||||||
## 2023.10.2
|
## 2023.10.2
|
||||||
|
|
||||||
|
11
locales/index.d.ts
vendored
11
locales/index.d.ts
vendored
@@ -982,6 +982,7 @@ export interface Locale {
|
|||||||
"unassign": string;
|
"unassign": string;
|
||||||
"color": string;
|
"color": string;
|
||||||
"manageCustomEmojis": string;
|
"manageCustomEmojis": string;
|
||||||
|
"manageAvatarDecorations": string;
|
||||||
"youCannotCreateAnymore": string;
|
"youCannotCreateAnymore": string;
|
||||||
"cannotPerformTemporary": string;
|
"cannotPerformTemporary": string;
|
||||||
"cannotPerformTemporaryDescription": string;
|
"cannotPerformTemporaryDescription": string;
|
||||||
@@ -1152,6 +1153,11 @@ export interface Locale {
|
|||||||
"angle": string;
|
"angle": string;
|
||||||
"flip": string;
|
"flip": string;
|
||||||
"showAvatarDecorations": string;
|
"showAvatarDecorations": string;
|
||||||
|
"releaseToRefresh": string;
|
||||||
|
"refreshing": string;
|
||||||
|
"pullDownToRefresh": string;
|
||||||
|
"disableStreamingTimeline": string;
|
||||||
|
"useGroupedNotifications": string;
|
||||||
"_announcement": {
|
"_announcement": {
|
||||||
"forExistingUsers": string;
|
"forExistingUsers": string;
|
||||||
"forExistingUsersDescription": string;
|
"forExistingUsersDescription": string;
|
||||||
@@ -1571,6 +1577,7 @@ export interface Locale {
|
|||||||
"inviteLimitCycle": string;
|
"inviteLimitCycle": string;
|
||||||
"inviteExpirationTime": string;
|
"inviteExpirationTime": string;
|
||||||
"canManageCustomEmojis": string;
|
"canManageCustomEmojis": string;
|
||||||
|
"canManageAvatarDecorations": string;
|
||||||
"driveCapacity": string;
|
"driveCapacity": string;
|
||||||
"alwaysMarkNsfw": string;
|
"alwaysMarkNsfw": string;
|
||||||
"pinMax": string;
|
"pinMax": string;
|
||||||
@@ -1707,6 +1714,7 @@ export interface Locale {
|
|||||||
"donate": string;
|
"donate": string;
|
||||||
"morePatrons": string;
|
"morePatrons": string;
|
||||||
"patrons": string;
|
"patrons": string;
|
||||||
|
"projectMembers": string;
|
||||||
};
|
};
|
||||||
"_displayOfSensitiveMedia": {
|
"_displayOfSensitiveMedia": {
|
||||||
"respect": string;
|
"respect": string;
|
||||||
@@ -2193,6 +2201,9 @@ export interface Locale {
|
|||||||
"checkNotificationBehavior": string;
|
"checkNotificationBehavior": string;
|
||||||
"sendTestNotification": string;
|
"sendTestNotification": string;
|
||||||
"notificationWillBeDisplayedLikeThis": string;
|
"notificationWillBeDisplayedLikeThis": string;
|
||||||
|
"reactedBySomeUsers": string;
|
||||||
|
"renotedBySomeUsers": string;
|
||||||
|
"followedBySomeUsers": string;
|
||||||
"_types": {
|
"_types": {
|
||||||
"all": string;
|
"all": string;
|
||||||
"note": string;
|
"note": string;
|
||||||
|
@@ -979,6 +979,7 @@ assign: "アサイン"
|
|||||||
unassign: "アサインを解除"
|
unassign: "アサインを解除"
|
||||||
color: "色"
|
color: "色"
|
||||||
manageCustomEmojis: "カスタム絵文字の管理"
|
manageCustomEmojis: "カスタム絵文字の管理"
|
||||||
|
manageAvatarDecorations: "アバターデコレーションの管理"
|
||||||
youCannotCreateAnymore: "これ以上作成することはできません。"
|
youCannotCreateAnymore: "これ以上作成することはできません。"
|
||||||
cannotPerformTemporary: "一時的に利用できません"
|
cannotPerformTemporary: "一時的に利用できません"
|
||||||
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
|
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
|
||||||
@@ -1149,6 +1150,11 @@ detach: "外す"
|
|||||||
angle: "角度"
|
angle: "角度"
|
||||||
flip: "反転"
|
flip: "反転"
|
||||||
showAvatarDecorations: "アイコンのデコレーションを表示"
|
showAvatarDecorations: "アイコンのデコレーションを表示"
|
||||||
|
releaseToRefresh: "離してリロード"
|
||||||
|
refreshing: "リロード中"
|
||||||
|
pullDownToRefresh: "引っ張ってリロード"
|
||||||
|
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
|
||||||
|
useGroupedNotifications: "通知をグルーピングして表示する"
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
@@ -1492,6 +1498,7 @@ _role:
|
|||||||
inviteLimitCycle: "招待コードの発行間隔"
|
inviteLimitCycle: "招待コードの発行間隔"
|
||||||
inviteExpirationTime: "招待コードの有効期限"
|
inviteExpirationTime: "招待コードの有効期限"
|
||||||
canManageCustomEmojis: "カスタム絵文字の管理"
|
canManageCustomEmojis: "カスタム絵文字の管理"
|
||||||
|
canManageAvatarDecorations: "アバターデコレーションの管理"
|
||||||
driveCapacity: "ドライブ容量"
|
driveCapacity: "ドライブ容量"
|
||||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||||
pinMax: "ノートのピン留めの最大数"
|
pinMax: "ノートのピン留めの最大数"
|
||||||
@@ -1617,13 +1624,14 @@ _registry:
|
|||||||
|
|
||||||
_aboutMisskey:
|
_aboutMisskey:
|
||||||
about: "Misskeyはsyuiloによって2014年から開発されている、オープンソースのソフトウェアです。"
|
about: "Misskeyはsyuiloによって2014年から開発されている、オープンソースのソフトウェアです。"
|
||||||
contributors: "主なコントリビューター"
|
contributors: "コントリビューター"
|
||||||
allContributors: "全てのコントリビューター"
|
allContributors: "全てのコントリビューター"
|
||||||
source: "ソースコード"
|
source: "ソースコード"
|
||||||
translation: "Misskeyを翻訳"
|
translation: "Misskeyを翻訳"
|
||||||
donate: "Misskeyに寄付"
|
donate: "Misskeyに寄付"
|
||||||
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
|
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
|
||||||
patrons: "支援者"
|
patrons: "支援者"
|
||||||
|
projectMembers: "プロジェクトメンバー"
|
||||||
|
|
||||||
_displayOfSensitiveMedia:
|
_displayOfSensitiveMedia:
|
||||||
respect: "センシティブ設定されたメディアを隠す"
|
respect: "センシティブ設定されたメディアを隠す"
|
||||||
@@ -2107,6 +2115,9 @@ _notification:
|
|||||||
checkNotificationBehavior: "通知の表示を確かめる"
|
checkNotificationBehavior: "通知の表示を確かめる"
|
||||||
sendTestNotification: "テスト通知を送信する"
|
sendTestNotification: "テスト通知を送信する"
|
||||||
notificationWillBeDisplayedLikeThis: "通知はこのように表示されます"
|
notificationWillBeDisplayedLikeThis: "通知はこのように表示されます"
|
||||||
|
reactedBySomeUsers: "{n}人がリアクションしました"
|
||||||
|
renotedBySomeUsers: "{n}人がリノートしました"
|
||||||
|
followedBySomeUsers: "{n}人にフォローされました"
|
||||||
|
|
||||||
_types:
|
_types:
|
||||||
all: "すべて"
|
all: "すべて"
|
||||||
|
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2023.11.0-beta.4",
|
"version": "2023.11.0-beta.7",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -48,14 +48,14 @@
|
|||||||
"cssnano": "6.0.1",
|
"cssnano": "6.0.1",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
"terser": "5.22.0",
|
"terser": "5.24.0",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "6.9.0",
|
"@typescript-eslint/eslint-plugin": "6.9.1",
|
||||||
"@typescript-eslint/parser": "6.9.0",
|
"@typescript-eslint/parser": "6.9.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.3.3",
|
"cypress": "13.4.0",
|
||||||
"eslint": "8.52.0",
|
"eslint": "8.52.0",
|
||||||
"start-server-and-test": "2.0.1"
|
"start-server-and-test": "2.0.1"
|
||||||
},
|
},
|
||||||
|
BIN
packages/backend/assets/tabler-badges/bell.png
Normal file
BIN
packages/backend/assets/tabler-badges/bell.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
@@ -66,17 +66,17 @@
|
|||||||
"@discordapp/twemoji": "14.1.2",
|
"@discordapp/twemoji": "14.1.2",
|
||||||
"@fastify/accepts": "4.2.0",
|
"@fastify/accepts": "4.2.0",
|
||||||
"@fastify/cookie": "9.1.0",
|
"@fastify/cookie": "9.1.0",
|
||||||
"@fastify/cors": "8.4.0",
|
"@fastify/cors": "8.4.1",
|
||||||
"@fastify/express": "2.3.0",
|
"@fastify/express": "2.3.0",
|
||||||
"@fastify/http-proxy": "9.2.1",
|
"@fastify/http-proxy": "9.2.1",
|
||||||
"@fastify/multipart": "8.0.0",
|
"@fastify/multipart": "8.0.0",
|
||||||
"@fastify/static": "6.11.2",
|
"@fastify/static": "6.12.0",
|
||||||
"@fastify/view": "8.2.0",
|
"@fastify/view": "8.2.0",
|
||||||
"@nestjs/common": "10.2.7",
|
"@nestjs/common": "10.2.7",
|
||||||
"@nestjs/core": "10.2.7",
|
"@nestjs/core": "10.2.7",
|
||||||
"@nestjs/testing": "10.2.7",
|
"@nestjs/testing": "10.2.7",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@simplewebauthn/server": "8.3.4",
|
"@simplewebauthn/server": "8.3.5",
|
||||||
"@sinonjs/fake-timers": "11.2.2",
|
"@sinonjs/fake-timers": "11.2.2",
|
||||||
"@swc/cli": "0.1.62",
|
"@swc/cli": "0.1.62",
|
||||||
"@swc/core": "1.3.95",
|
"@swc/core": "1.3.95",
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.2",
|
||||||
"bullmq": "4.12.6",
|
"bullmq": "4.12.7",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "9.0.1",
|
"cbor": "9.0.1",
|
||||||
"chalk": "5.3.0",
|
"chalk": "5.3.0",
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.2",
|
||||||
"punycode": "2.3.0",
|
"punycode": "2.3.1",
|
||||||
"pureimage": "0.3.17",
|
"pureimage": "0.3.17",
|
||||||
"qrcode": "1.5.3",
|
"qrcode": "1.5.3",
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
"@simplewebauthn/typescript-types": "8.3.4",
|
"@simplewebauthn/typescript-types": "8.3.4",
|
||||||
"@swc/jest": "0.2.29",
|
"@swc/jest": "0.2.29",
|
||||||
"@types/accepts": "1.3.6",
|
"@types/accepts": "1.3.6",
|
||||||
"@types/archiver": "5.3.4",
|
"@types/archiver": "6.0.0",
|
||||||
"@types/bcryptjs": "2.4.5",
|
"@types/bcryptjs": "2.4.5",
|
||||||
"@types/body-parser": "1.19.4",
|
"@types/body-parser": "1.19.4",
|
||||||
"@types/cbor": "6.0.0",
|
"@types/cbor": "6.0.0",
|
||||||
@@ -183,14 +183,14 @@
|
|||||||
"@types/content-disposition": "0.5.7",
|
"@types/content-disposition": "0.5.7",
|
||||||
"@types/fluent-ffmpeg": "2.1.23",
|
"@types/fluent-ffmpeg": "2.1.23",
|
||||||
"@types/http-link-header": "1.0.4",
|
"@types/http-link-header": "1.0.4",
|
||||||
"@types/jest": "29.5.6",
|
"@types/jest": "29.5.7",
|
||||||
"@types/js-yaml": "4.0.8",
|
"@types/js-yaml": "4.0.8",
|
||||||
"@types/jsdom": "21.1.4",
|
"@types/jsdom": "21.1.4",
|
||||||
"@types/jsonld": "1.5.11",
|
"@types/jsonld": "1.5.11",
|
||||||
"@types/jsrsasign": "10.5.11",
|
"@types/jsrsasign": "10.5.11",
|
||||||
"@types/mime-types": "2.1.3",
|
"@types/mime-types": "2.1.3",
|
||||||
"@types/ms": "0.7.33",
|
"@types/ms": "0.7.33",
|
||||||
"@types/node": "20.8.9",
|
"@types/node": "20.8.10",
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.13",
|
"@types/nodemailer": "6.4.13",
|
||||||
"@types/oauth": "0.9.3",
|
"@types/oauth": "0.9.3",
|
||||||
@@ -213,8 +213,8 @@
|
|||||||
"@types/vary": "1.1.2",
|
"@types/vary": "1.1.2",
|
||||||
"@types/web-push": "3.6.2",
|
"@types/web-push": "3.6.2",
|
||||||
"@types/ws": "8.5.8",
|
"@types/ws": "8.5.8",
|
||||||
"@typescript-eslint/eslint-plugin": "6.9.0",
|
"@typescript-eslint/eslint-plugin": "6.9.1",
|
||||||
"@typescript-eslint/parser": "6.9.0",
|
"@typescript-eslint/parser": "6.9.1",
|
||||||
"aws-sdk-client-mock": "3.0.0",
|
"aws-sdk-client-mock": "3.0.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.52.0",
|
"eslint": "8.52.0",
|
||||||
|
@@ -258,7 +258,7 @@ export function loadConfig(): Config {
|
|||||||
clientEntry: clientManifest['src/_boot_.ts'],
|
clientEntry: clientManifest['src/_boot_.ts'],
|
||||||
clientManifestExists: clientManifestExists,
|
clientManifestExists: clientManifestExists,
|
||||||
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
||||||
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 300,
|
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
|
||||||
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
||||||
pidFile: config.pidFile,
|
pidFile: config.pidFile,
|
||||||
};
|
};
|
||||||
|
@@ -100,17 +100,14 @@ class NotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deliver() {
|
public async notify() {
|
||||||
for (const x of this.queue) {
|
for (const x of this.queue) {
|
||||||
// ミュート情報を取得
|
if (x.reason === 'renote') {
|
||||||
const mentioneeMutes = await this.mutingsRepository.findBy({
|
this.notificationService.createNotification(x.target, 'renote', {
|
||||||
muterId: x.target,
|
noteId: this.note.id,
|
||||||
});
|
targetNoteId: this.note.renoteId!,
|
||||||
|
}, this.notifier.id);
|
||||||
const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId);
|
} else {
|
||||||
|
|
||||||
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
|
||||||
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
|
|
||||||
this.notificationService.createNotification(x.target, x.reason, {
|
this.notificationService.createNotification(x.target, x.reason, {
|
||||||
noteId: this.note.id,
|
noteId: this.note.id,
|
||||||
}, this.notifier.id);
|
}, this.notifier.id);
|
||||||
@@ -642,7 +639,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nm.deliver();
|
nm.notify();
|
||||||
|
|
||||||
//#region AP deliver
|
//#region AP deliver
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
|
@@ -24,6 +24,7 @@ import { bindThis } from '@/decorators.js';
|
|||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { SearchService } from '@/core/SearchService.js';
|
import { SearchService } from '@/core/SearchService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteDeleteService {
|
export class NoteDeleteService {
|
||||||
@@ -77,8 +78,8 @@ export class NoteDeleteService {
|
|||||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
||||||
let renote: MiNote | null = null;
|
let renote: MiNote | null = null;
|
||||||
|
|
||||||
// if deletd note is renote
|
// if deleted note is renote
|
||||||
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
|
if (isPureRenote(note)) {
|
||||||
renote = await this.notesRepository.findOneBy({
|
renote = await this.notesRepository.findOneBy({
|
||||||
id: note.renoteId,
|
id: note.renoteId,
|
||||||
});
|
});
|
||||||
|
@@ -19,6 +19,7 @@ import { IdService } from '@/core/IdService.js';
|
|||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { UserListService } from '@/core/UserListService.js';
|
import { UserListService } from '@/core/UserListService.js';
|
||||||
|
import type { FilterUnionByProperty } from '@/types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService implements OnApplicationShutdown {
|
export class NotificationService implements OnApplicationShutdown {
|
||||||
@@ -73,10 +74,10 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createNotification(
|
public async createNotification<T extends MiNotification['type']>(
|
||||||
notifieeId: MiUser['id'],
|
notifieeId: MiUser['id'],
|
||||||
type: MiNotification['type'],
|
type: T,
|
||||||
data: Omit<Partial<MiNotification>, 'notifierId'>,
|
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
||||||
notifierId?: MiUser['id'] | null,
|
notifierId?: MiUser['id'] | null,
|
||||||
): Promise<MiNotification | null> {
|
): Promise<MiNotification | null> {
|
||||||
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
|
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
|
||||||
@@ -128,9 +129,11 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
type: type,
|
type: type,
|
||||||
notifierId: notifierId,
|
...(notifierId ? {
|
||||||
|
notifierId,
|
||||||
|
} : {}),
|
||||||
...data,
|
...data,
|
||||||
} as MiNotification;
|
} as any as FilterUnionByProperty<MiNotification, 'type', T>;
|
||||||
|
|
||||||
const redisIdPromise = this.redisClient.xadd(
|
const redisIdPromise = this.redisClient.xadd(
|
||||||
`notificationTimeline:${notifieeId}`,
|
`notificationTimeline:${notifieeId}`,
|
||||||
@@ -144,7 +147,9 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
||||||
|
|
||||||
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
||||||
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
|
// テスト通知の場合は即時発行
|
||||||
|
const interval = notification.type === 'test' ? 0 : 2000;
|
||||||
|
setTimeout(interval, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
|
||||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
|
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
|
||||||
if (latestReadNotificationId && (latestReadNotificationId >= (await redisIdPromise)!)) return;
|
if (latestReadNotificationId && (latestReadNotificationId >= (await redisIdPromise)!)) return;
|
||||||
|
|
||||||
|
@@ -32,6 +32,7 @@ export type RolePolicies = {
|
|||||||
inviteLimitCycle: number;
|
inviteLimitCycle: number;
|
||||||
inviteExpirationTime: number;
|
inviteExpirationTime: number;
|
||||||
canManageCustomEmojis: boolean;
|
canManageCustomEmojis: boolean;
|
||||||
|
canManageAvatarDecorations: boolean;
|
||||||
canSearchNotes: boolean;
|
canSearchNotes: boolean;
|
||||||
canUseTranslator: boolean;
|
canUseTranslator: boolean;
|
||||||
canHideAds: boolean;
|
canHideAds: boolean;
|
||||||
@@ -57,6 +58,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||||||
inviteLimitCycle: 60 * 24 * 7,
|
inviteLimitCycle: 60 * 24 * 7,
|
||||||
inviteExpirationTime: 0,
|
inviteExpirationTime: 0,
|
||||||
canManageCustomEmojis: false,
|
canManageCustomEmojis: false,
|
||||||
|
canManageAvatarDecorations: false,
|
||||||
canSearchNotes: false,
|
canSearchNotes: false,
|
||||||
canUseTranslator: true,
|
canUseTranslator: true,
|
||||||
canHideAds: false,
|
canHideAds: false,
|
||||||
@@ -306,6 +308,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
|
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
|
||||||
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
||||||
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
||||||
|
canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)),
|
||||||
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||||
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
||||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||||
|
@@ -509,7 +509,6 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
|
|
||||||
// 通知を作成
|
// 通知を作成
|
||||||
this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
||||||
followRequestId: followRequest.id,
|
|
||||||
}, follower.id);
|
}, follower.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -495,6 +495,7 @@ export class ApRendererService {
|
|||||||
preferredUsername: user.username,
|
preferredUsername: user.username,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
||||||
|
_misskey_summary: profile.description,
|
||||||
icon: avatar ? this.renderImage(avatar) : null,
|
icon: avatar ? this.renderImage(avatar) : null,
|
||||||
image: banner ? this.renderImage(banner) : null,
|
image: banner ? this.renderImage(banner) : null,
|
||||||
tag,
|
tag,
|
||||||
@@ -644,6 +645,7 @@ export class ApRendererService {
|
|||||||
'_misskey_quote': 'misskey:_misskey_quote',
|
'_misskey_quote': 'misskey:_misskey_quote',
|
||||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||||
'_misskey_votes': 'misskey:_misskey_votes',
|
'_misskey_votes': 'misskey:_misskey_votes',
|
||||||
|
'_misskey_summary': 'misskey:_misskey_summary',
|
||||||
'isCat': 'misskey:isCat',
|
'isCat': 'misskey:isCat',
|
||||||
// vcard
|
// vcard
|
||||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||||
|
@@ -319,9 +319,17 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
emojis,
|
emojis,
|
||||||
})) as MiRemoteUser;
|
})) as MiRemoteUser;
|
||||||
|
|
||||||
|
let _description: string | null = null;
|
||||||
|
|
||||||
|
if (person._misskey_summary) {
|
||||||
|
_description = truncate(person._misskey_summary, summaryLength);
|
||||||
|
} else if (person.summary) {
|
||||||
|
_description = this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag);
|
||||||
|
}
|
||||||
|
|
||||||
await transactionalEntityManager.save(new MiUserProfile({
|
await transactionalEntityManager.save(new MiUserProfile({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
description: _description,
|
||||||
url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
birthday: bday?.[0] ?? null,
|
birthday: bday?.[0] ?? null,
|
||||||
@@ -487,10 +495,18 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _description: string | null = null;
|
||||||
|
|
||||||
|
if (person._misskey_summary) {
|
||||||
|
_description = truncate(person._misskey_summary, summaryLength);
|
||||||
|
} else if (person.summary) {
|
||||||
|
_description = this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag);
|
||||||
|
}
|
||||||
|
|
||||||
await this.userProfilesRepository.update({ userId: exist.id }, {
|
await this.userProfilesRepository.update({ userId: exist.id }, {
|
||||||
url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
description: _description,
|
||||||
birthday: bday?.[0] ?? null,
|
birthday: bday?.[0] ?? null,
|
||||||
location: person['vcard:Address'] ?? null,
|
location: person['vcard:Address'] ?? null,
|
||||||
});
|
});
|
||||||
|
@@ -12,6 +12,7 @@ export interface IObject {
|
|||||||
id?: string;
|
id?: string;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
|
_misskey_summary?: string;
|
||||||
published?: string;
|
published?: string;
|
||||||
cc?: ApObject;
|
cc?: ApObject;
|
||||||
to?: ApObject;
|
to?: ApObject;
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository, NotesRepository } from '@/models/_.js';
|
import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NotesRepository } from '@/models/_.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { } from '@/models/Blocking.js';
|
import type { } from '@/models/Blocking.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
@@ -31,9 +31,6 @@ export class ChannelEntityService {
|
|||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@Inject(DI.noteUnreadsRepository)
|
|
||||||
private noteUnreadsRepository: NoteUnreadsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.driveFilesRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
@@ -54,13 +51,6 @@ export class ChannelEntityService {
|
|||||||
|
|
||||||
const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
|
const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
|
||||||
|
|
||||||
const hasUnreadNote = meId ? await this.noteUnreadsRepository.exist({
|
|
||||||
where: {
|
|
||||||
noteChannelId: channel.id,
|
|
||||||
userId: meId,
|
|
||||||
},
|
|
||||||
}) : undefined;
|
|
||||||
|
|
||||||
const isFollowing = meId ? await this.channelFollowingsRepository.exist({
|
const isFollowing = meId ? await this.channelFollowingsRepository.exist({
|
||||||
where: {
|
where: {
|
||||||
followerId: meId,
|
followerId: meId,
|
||||||
@@ -99,7 +89,7 @@ export class ChannelEntityService {
|
|||||||
...(me ? {
|
...(me ? {
|
||||||
isFollowing,
|
isFollowing,
|
||||||
isFavorited,
|
isFavorited,
|
||||||
hasUnreadNote,
|
hasUnreadNote: false, // 後方互換性のため
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
||||||
...(detailed ? {
|
...(detailed ? {
|
||||||
|
@@ -7,20 +7,21 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { AccessTokensRepository, FollowRequestsRepository, NotesRepository, MiUser, UsersRepository } from '@/models/_.js';
|
import type { FollowRequestsRepository, NotesRepository, MiUser, UsersRepository } from '@/models/_.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import type { MiNotification } from '@/models/Notification.js';
|
import type { MiGroupedNotification, MiNotification } from '@/models/Notification.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
import { isNotNull } from '@/misc/is-not-null.js';
|
||||||
import { notificationTypes } from '@/types.js';
|
import { FilterUnionByProperty, notificationTypes } from '@/types.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
||||||
import type { UserEntityService } from './UserEntityService.js';
|
import type { UserEntityService } from './UserEntityService.js';
|
||||||
import type { NoteEntityService } from './NoteEntityService.js';
|
import type { NoteEntityService } from './NoteEntityService.js';
|
||||||
|
|
||||||
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]);
|
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]);
|
||||||
|
const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded']);
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationEntityService implements OnModuleInit {
|
export class NotificationEntityService implements OnModuleInit {
|
||||||
@@ -40,9 +41,6 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
@Inject(DI.followRequestsRepository)
|
@Inject(DI.followRequestsRepository)
|
||||||
private followRequestsRepository: FollowRequestsRepository,
|
private followRequestsRepository: FollowRequestsRepository,
|
||||||
|
|
||||||
@Inject(DI.accessTokensRepository)
|
|
||||||
private accessTokensRepository: AccessTokensRepository,
|
|
||||||
|
|
||||||
//private userEntityService: UserEntityService,
|
//private userEntityService: UserEntityService,
|
||||||
//private noteEntityService: NoteEntityService,
|
//private noteEntityService: NoteEntityService,
|
||||||
//private customEmojiService: CustomEmojiService,
|
//private customEmojiService: CustomEmojiService,
|
||||||
@@ -69,18 +67,17 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
},
|
},
|
||||||
): Promise<Packed<'Notification'>> {
|
): Promise<Packed<'Notification'>> {
|
||||||
const notification = src;
|
const notification = src;
|
||||||
const token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
|
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? (
|
||||||
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? (
|
|
||||||
hint?.packedNotes != null
|
hint?.packedNotes != null
|
||||||
? hint.packedNotes.get(notification.noteId)
|
? hint.packedNotes.get(notification.noteId)
|
||||||
: this.noteEntityService.pack(notification.noteId!, { id: meId }, {
|
: this.noteEntityService.pack(notification.noteId, { id: meId }, {
|
||||||
detail: true,
|
detail: true,
|
||||||
})
|
})
|
||||||
) : undefined;
|
) : undefined;
|
||||||
const userIfNeed = notification.notifierId != null ? (
|
const userIfNeed = 'notifierId' in notification ? (
|
||||||
hint?.packedUsers != null
|
hint?.packedUsers != null
|
||||||
? hint.packedUsers.get(notification.notifierId)
|
? hint.packedUsers.get(notification.notifierId)
|
||||||
: this.userEntityService.pack(notification.notifierId!, { id: meId }, {
|
: this.userEntityService.pack(notification.notifierId, { id: meId }, {
|
||||||
detail: false,
|
detail: false,
|
||||||
})
|
})
|
||||||
) : undefined;
|
) : undefined;
|
||||||
@@ -89,7 +86,7 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
id: notification.id,
|
id: notification.id,
|
||||||
createdAt: new Date(notification.createdAt).toISOString(),
|
createdAt: new Date(notification.createdAt).toISOString(),
|
||||||
type: notification.type,
|
type: notification.type,
|
||||||
userId: notification.notifierId,
|
userId: 'notifierId' in notification ? notification.notifierId : undefined,
|
||||||
...(userIfNeed != null ? { user: userIfNeed } : {}),
|
...(userIfNeed != null ? { user: userIfNeed } : {}),
|
||||||
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
|
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
|
||||||
...(notification.type === 'reaction' ? {
|
...(notification.type === 'reaction' ? {
|
||||||
@@ -100,8 +97,8 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
} : {}),
|
} : {}),
|
||||||
...(notification.type === 'app' ? {
|
...(notification.type === 'app' ? {
|
||||||
body: notification.customBody,
|
body: notification.customBody,
|
||||||
header: notification.customHeader ?? token?.name,
|
header: notification.customHeader,
|
||||||
icon: notification.customIcon ?? token?.iconUrl,
|
icon: notification.customIcon,
|
||||||
} : {}),
|
} : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -115,7 +112,7 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
|
|
||||||
let validNotifications = notifications;
|
let validNotifications = notifications;
|
||||||
|
|
||||||
const noteIds = validNotifications.map(x => x.noteId).filter(isNotNull);
|
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
|
||||||
const notes = noteIds.length > 0 ? await this.notesRepository.find({
|
const notes = noteIds.length > 0 ? await this.notesRepository.find({
|
||||||
where: { id: In(noteIds) },
|
where: { id: In(noteIds) },
|
||||||
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
|
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
|
||||||
@@ -125,9 +122,9 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
|
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
|
||||||
|
|
||||||
validNotifications = validNotifications.filter(x => x.noteId == null || packedNotes.has(x.noteId));
|
validNotifications = validNotifications.filter(x => !('noteId' in x) || packedNotes.has(x.noteId));
|
||||||
|
|
||||||
const userIds = validNotifications.map(x => x.notifierId).filter(isNotNull);
|
const userIds = validNotifications.map(x => 'notifierId' in x ? x.notifierId : null).filter(isNotNull);
|
||||||
const users = userIds.length > 0 ? await this.usersRepository.find({
|
const users = userIds.length > 0 ? await this.usersRepository.find({
|
||||||
where: { id: In(userIds) },
|
where: { id: In(userIds) },
|
||||||
}) : [];
|
}) : [];
|
||||||
@@ -137,10 +134,10 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
|
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
|
||||||
|
|
||||||
// 既に解決されたフォローリクエストの通知を除外
|
// 既に解決されたフォローリクエストの通知を除外
|
||||||
const followRequestNotifications = validNotifications.filter(x => x.type === 'receiveFollowRequest');
|
const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty<MiGroupedNotification, 'type', 'receiveFollowRequest'> => x.type === 'receiveFollowRequest');
|
||||||
if (followRequestNotifications.length > 0) {
|
if (followRequestNotifications.length > 0) {
|
||||||
const reqs = await this.followRequestsRepository.find({
|
const reqs = await this.followRequestsRepository.find({
|
||||||
where: { followerId: In(followRequestNotifications.map(x => x.notifierId!)) },
|
where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) },
|
||||||
});
|
});
|
||||||
validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
|
validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
|
||||||
}
|
}
|
||||||
@@ -150,4 +147,141 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
packedUsers,
|
packedUsers,
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async packGrouped(
|
||||||
|
src: MiGroupedNotification,
|
||||||
|
meId: MiUser['id'],
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
options: {
|
||||||
|
|
||||||
|
},
|
||||||
|
hint?: {
|
||||||
|
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
|
||||||
|
packedUsers: Map<MiUser['id'], Packed<'User'>>;
|
||||||
|
},
|
||||||
|
): Promise<Packed<'Notification'>> {
|
||||||
|
const notification = src;
|
||||||
|
const noteIfNeed = NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? (
|
||||||
|
hint?.packedNotes != null
|
||||||
|
? hint.packedNotes.get(notification.noteId)
|
||||||
|
: this.noteEntityService.pack(notification.noteId, { id: meId }, {
|
||||||
|
detail: true,
|
||||||
|
})
|
||||||
|
) : undefined;
|
||||||
|
const userIfNeed = 'notifierId' in notification ? (
|
||||||
|
hint?.packedUsers != null
|
||||||
|
? hint.packedUsers.get(notification.notifierId)
|
||||||
|
: this.userEntityService.pack(notification.notifierId, { id: meId }, {
|
||||||
|
detail: false,
|
||||||
|
})
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
|
if (notification.type === 'reaction:grouped') {
|
||||||
|
const reactions = await Promise.all(notification.reactions.map(async reaction => {
|
||||||
|
const user = hint?.packedUsers != null
|
||||||
|
? hint.packedUsers.get(reaction.userId)!
|
||||||
|
: await this.userEntityService.pack(reaction.userId, { id: meId }, {
|
||||||
|
detail: false,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
reaction: reaction.reaction,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
return await awaitAll({
|
||||||
|
id: notification.id,
|
||||||
|
createdAt: new Date(notification.createdAt).toISOString(),
|
||||||
|
type: notification.type,
|
||||||
|
note: noteIfNeed,
|
||||||
|
reactions,
|
||||||
|
});
|
||||||
|
} else if (notification.type === 'renote:grouped') {
|
||||||
|
const users = await Promise.all(notification.userIds.map(userId => {
|
||||||
|
const user = hint?.packedUsers != null
|
||||||
|
? hint.packedUsers.get(userId)
|
||||||
|
: this.userEntityService.pack(userId!, { id: meId }, {
|
||||||
|
detail: false,
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}));
|
||||||
|
return await awaitAll({
|
||||||
|
id: notification.id,
|
||||||
|
createdAt: new Date(notification.createdAt).toISOString(),
|
||||||
|
type: notification.type,
|
||||||
|
note: noteIfNeed,
|
||||||
|
users,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await awaitAll({
|
||||||
|
id: notification.id,
|
||||||
|
createdAt: new Date(notification.createdAt).toISOString(),
|
||||||
|
type: notification.type,
|
||||||
|
userId: 'notifierId' in notification ? notification.notifierId : undefined,
|
||||||
|
...(userIfNeed != null ? { user: userIfNeed } : {}),
|
||||||
|
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
|
||||||
|
...(notification.type === 'reaction' ? {
|
||||||
|
reaction: notification.reaction,
|
||||||
|
} : {}),
|
||||||
|
...(notification.type === 'achievementEarned' ? {
|
||||||
|
achievement: notification.achievement,
|
||||||
|
} : {}),
|
||||||
|
...(notification.type === 'app' ? {
|
||||||
|
body: notification.customBody,
|
||||||
|
header: notification.customHeader,
|
||||||
|
icon: notification.customIcon,
|
||||||
|
} : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async packGroupedMany(
|
||||||
|
notifications: MiGroupedNotification[],
|
||||||
|
meId: MiUser['id'],
|
||||||
|
) {
|
||||||
|
if (notifications.length === 0) return [];
|
||||||
|
|
||||||
|
let validNotifications = notifications;
|
||||||
|
|
||||||
|
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
|
||||||
|
const notes = noteIds.length > 0 ? await this.notesRepository.find({
|
||||||
|
where: { id: In(noteIds) },
|
||||||
|
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
|
||||||
|
}) : [];
|
||||||
|
const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, {
|
||||||
|
detail: true,
|
||||||
|
});
|
||||||
|
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
|
||||||
|
|
||||||
|
validNotifications = validNotifications.filter(x => !('noteId' in x) || packedNotes.has(x.noteId));
|
||||||
|
|
||||||
|
const userIds = [];
|
||||||
|
for (const notification of validNotifications) {
|
||||||
|
if ('notifierId' in notification) userIds.push(notification.notifierId);
|
||||||
|
if (notification.type === 'reaction:grouped') userIds.push(...notification.reactions.map(x => x.userId));
|
||||||
|
if (notification.type === 'renote:grouped') userIds.push(...notification.userIds);
|
||||||
|
}
|
||||||
|
const users = userIds.length > 0 ? await this.usersRepository.find({
|
||||||
|
where: { id: In(userIds) },
|
||||||
|
}) : [];
|
||||||
|
const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, {
|
||||||
|
detail: false,
|
||||||
|
});
|
||||||
|
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
|
||||||
|
|
||||||
|
// 既に解決されたフォローリクエストの通知を除外
|
||||||
|
const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty<MiGroupedNotification, 'type', 'receiveFollowRequest'> => x.type === 'receiveFollowRequest');
|
||||||
|
if (followRequestNotifications.length > 0) {
|
||||||
|
const reqs = await this.followRequestsRepository.find({
|
||||||
|
where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) },
|
||||||
|
});
|
||||||
|
validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Promise.all(validNotifications.map(x => this.packGrouped(x, meId, {}, {
|
||||||
|
packedNotes,
|
||||||
|
packedUsers,
|
||||||
|
})));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
|
|||||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||||
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js';
|
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js';
|
||||||
|
import { MiNotification } from '@/models/Notification.js';
|
||||||
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js';
|
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
@@ -235,17 +236,34 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getHasUnreadNotification(userId: MiUser['id']): Promise<boolean> {
|
public async getNotificationsInfo(userId: MiUser['id']): Promise<{
|
||||||
|
hasUnread: boolean;
|
||||||
|
unreadCount: number;
|
||||||
|
}> {
|
||||||
|
const response = {
|
||||||
|
hasUnread: false,
|
||||||
|
unreadCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
||||||
|
|
||||||
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
if (!latestReadNotificationId) {
|
||||||
`notificationTimeline:${userId}`,
|
response.unreadCount = await this.redisClient.xlen(`notificationTimeline:${userId}`);
|
||||||
'+',
|
} else {
|
||||||
'-',
|
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
||||||
'COUNT', 1);
|
`notificationTimeline:${userId}`,
|
||||||
const latestNotificationId = latestNotificationIdsRes[0]?.[0];
|
'+',
|
||||||
|
latestReadNotificationId,
|
||||||
|
);
|
||||||
|
|
||||||
return latestNotificationId != null && (latestReadNotificationId == null || latestReadNotificationId < latestNotificationId);
|
response.unreadCount = (latestNotificationIdsRes.length - 1 >= 0) ? latestNotificationIdsRes.length - 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.unreadCount > 0) {
|
||||||
|
response.hasUnread = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -331,6 +349,8 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
...announcement,
|
...announcement,
|
||||||
})) : null;
|
})) : null;
|
||||||
|
|
||||||
|
const notificationsInfo = isMe && opts.detail ? await this.getNotificationsInfo(user.id) : null;
|
||||||
|
|
||||||
const packed = {
|
const packed = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
@@ -449,8 +469,9 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
unreadAnnouncements,
|
unreadAnnouncements,
|
||||||
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
||||||
hasUnreadChannel: false, // 後方互換性のため
|
hasUnreadChannel: false, // 後方互換性のため
|
||||||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
hasUnreadNotification: notificationsInfo?.hasUnread, // 後方互換性のため
|
||||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||||
|
unreadNotificationsCount: notificationsInfo?.unreadCount,
|
||||||
mutedWords: profile!.mutedWords,
|
mutedWords: profile!.mutedWords,
|
||||||
mutedInstances: profile!.mutedInstances,
|
mutedInstances: profile!.mutedInstances,
|
||||||
mutingNotificationTypes: [], // 後方互換性のため
|
mutingNotificationTypes: [], // 後方互換性のため
|
||||||
|
10
packages/backend/src/misc/is-pure-renote.ts
Normal file
10
packages/backend/src/misc/is-pure-renote.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
|
||||||
|
export function isPureRenote(note: MiNote): note is MiNote & { renoteId: NonNullable<MiNote['renoteId']> } {
|
||||||
|
if (!note.renoteId) return false;
|
||||||
|
|
||||||
|
if (note.text) return false; // it's quoted with text
|
||||||
|
if (note.fileIds.length !== 0) return false; // it's quoted with files
|
||||||
|
if (note.hasPoll) return false; // it's quoted with poll
|
||||||
|
return true;
|
||||||
|
}
|
@@ -10,30 +10,73 @@ import { MiFollowRequest } from './FollowRequest.js';
|
|||||||
import { MiAccessToken } from './AccessToken.js';
|
import { MiAccessToken } from './AccessToken.js';
|
||||||
|
|
||||||
export type MiNotification = {
|
export type MiNotification = {
|
||||||
|
type: 'note';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
notifierId: MiUser['id'];
|
||||||
|
noteId: MiNote['id'];
|
||||||
|
} | {
|
||||||
|
type: 'follow';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
notifierId: MiUser['id'];
|
||||||
|
} | {
|
||||||
|
type: 'mention';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
notifierId: MiUser['id'];
|
||||||
|
noteId: MiNote['id'];
|
||||||
|
} | {
|
||||||
|
type: 'reply';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
notifierId: MiUser['id'];
|
||||||
|
noteId: MiNote['id'];
|
||||||
|
} | {
|
||||||
|
type: 'renote';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
notifierId: MiUser['id'];
|
||||||
|
noteId: MiNote['id'];
|
||||||
|
targetNoteId: MiNote['id'];
|
||||||
|
} | {
|
||||||
|
type: 'quote';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
notifierId: MiUser['id'];
|
||||||
|
noteId: MiNote['id'];
|
||||||
|
} | {
|
||||||
|
type: 'reaction';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
notifierId: MiUser['id'];
|
||||||
|
noteId: MiNote['id'];
|
||||||
|
reaction: string;
|
||||||
|
} | {
|
||||||
|
type: 'pollEnded';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
notifierId: MiUser['id'];
|
||||||
|
noteId: MiNote['id'];
|
||||||
|
} | {
|
||||||
|
type: 'receiveFollowRequest';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
notifierId: MiUser['id'];
|
||||||
|
} | {
|
||||||
|
type: 'followRequestAccepted';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
notifierId: MiUser['id'];
|
||||||
|
} | {
|
||||||
|
type: 'achievementEarned';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
achievement: string;
|
||||||
|
} | {
|
||||||
|
type: 'app';
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
// RedisのためDateではなくstring
|
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* 通知の送信者(initiator)
|
|
||||||
*/
|
|
||||||
notifierId: MiUser['id'] | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通知の種類。
|
|
||||||
*/
|
|
||||||
type: typeof notificationTypes[number];
|
|
||||||
|
|
||||||
noteId: MiNote['id'] | null;
|
|
||||||
|
|
||||||
followRequestId: MiFollowRequest['id'] | null;
|
|
||||||
|
|
||||||
reaction: string | null;
|
|
||||||
|
|
||||||
choice: number | null;
|
|
||||||
|
|
||||||
achievement: string | null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* アプリ通知のbody
|
* アプリ通知のbody
|
||||||
@@ -56,4 +99,25 @@ export type MiNotification = {
|
|||||||
* アプリ通知のアプリ(のトークン)
|
* アプリ通知のアプリ(のトークン)
|
||||||
*/
|
*/
|
||||||
appAccessTokenId: MiAccessToken['id'] | null;
|
appAccessTokenId: MiAccessToken['id'] | null;
|
||||||
}
|
} | {
|
||||||
|
type: 'test';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MiGroupedNotification = MiNotification | {
|
||||||
|
type: 'reaction:grouped';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
noteId: MiNote['id'];
|
||||||
|
reactions: {
|
||||||
|
userId: string;
|
||||||
|
reaction: string;
|
||||||
|
}[];
|
||||||
|
} | {
|
||||||
|
type: 'renote:grouped';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
noteId: MiNote['id'];
|
||||||
|
userIds: string[];
|
||||||
|
};
|
||||||
|
@@ -12,7 +12,6 @@ export const packedNotificationSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
example: 'xxxxxxxxxx',
|
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -22,7 +21,7 @@ export const packedNotificationSchema = {
|
|||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
enum: [...notificationTypes],
|
enum: [...notificationTypes, 'reaction:grouped', 'renote:grouped'],
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -63,5 +62,33 @@ export const packedNotificationSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
},
|
},
|
||||||
|
reactions: {
|
||||||
|
type: 'array',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
reaction: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['user', 'reaction'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
type: 'array',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -399,6 +399,10 @@ export const packedMeDetailedOnlySchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
|
unreadNotificationsCount: {
|
||||||
|
type: 'number',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
},
|
||||||
mutedWords: {
|
mutedWords: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
|
@@ -26,6 +26,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
|||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IActivity } from '@/core/activitypub/type.js';
|
import { IActivity } from '@/core/activitypub/type.js';
|
||||||
|
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||||
import type { FindOptionsWhere } from 'typeorm';
|
import type { FindOptionsWhere } from 'typeorm';
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ export class ActivityPubServerService {
|
|||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
private async packActivity(note: MiNote): Promise<any> {
|
private async packActivity(note: MiNote): Promise<any> {
|
||||||
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
|
if (isPureRenote(note)) {
|
||||||
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
|
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
|
||||||
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
|
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
|
||||||
}
|
}
|
||||||
|
@@ -217,6 +217,7 @@ import * as ep___i_importMuting from './endpoints/i/import-muting.js';
|
|||||||
import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js';
|
import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js';
|
||||||
import * as ep___i_importAntennas from './endpoints/i/import-antennas.js';
|
import * as ep___i_importAntennas from './endpoints/i/import-antennas.js';
|
||||||
import * as ep___i_notifications from './endpoints/i/notifications.js';
|
import * as ep___i_notifications from './endpoints/i/notifications.js';
|
||||||
|
import * as ep___i_notificationsGrouped from './endpoints/i/notifications-grouped.js';
|
||||||
import * as ep___i_pageLikes from './endpoints/i/page-likes.js';
|
import * as ep___i_pageLikes from './endpoints/i/page-likes.js';
|
||||||
import * as ep___i_pages from './endpoints/i/pages.js';
|
import * as ep___i_pages from './endpoints/i/pages.js';
|
||||||
import * as ep___i_pin from './endpoints/i/pin.js';
|
import * as ep___i_pin from './endpoints/i/pin.js';
|
||||||
@@ -574,6 +575,7 @@ const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep_
|
|||||||
const $i_importUserLists: Provider = { provide: 'ep:i/import-user-lists', useClass: ep___i_importUserLists.default };
|
const $i_importUserLists: Provider = { provide: 'ep:i/import-user-lists', useClass: ep___i_importUserLists.default };
|
||||||
const $i_importAntennas: Provider = { provide: 'ep:i/import-antennas', useClass: ep___i_importAntennas.default };
|
const $i_importAntennas: Provider = { provide: 'ep:i/import-antennas', useClass: ep___i_importAntennas.default };
|
||||||
const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep___i_notifications.default };
|
const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep___i_notifications.default };
|
||||||
|
const $i_notificationsGrouped: Provider = { provide: 'ep:i/notifications-grouped', useClass: ep___i_notificationsGrouped.default };
|
||||||
const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default };
|
const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default };
|
||||||
const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default };
|
const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default };
|
||||||
const $i_pin: Provider = { provide: 'ep:i/pin', useClass: ep___i_pin.default };
|
const $i_pin: Provider = { provide: 'ep:i/pin', useClass: ep___i_pin.default };
|
||||||
@@ -935,6 +937,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$i_importUserLists,
|
$i_importUserLists,
|
||||||
$i_importAntennas,
|
$i_importAntennas,
|
||||||
$i_notifications,
|
$i_notifications,
|
||||||
|
$i_notificationsGrouped,
|
||||||
$i_pageLikes,
|
$i_pageLikes,
|
||||||
$i_pages,
|
$i_pages,
|
||||||
$i_pin,
|
$i_pin,
|
||||||
@@ -1290,6 +1293,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$i_importUserLists,
|
$i_importUserLists,
|
||||||
$i_importAntennas,
|
$i_importAntennas,
|
||||||
$i_notifications,
|
$i_notifications,
|
||||||
|
$i_notificationsGrouped,
|
||||||
$i_pageLikes,
|
$i_pageLikes,
|
||||||
$i_pages,
|
$i_pages,
|
||||||
$i_pin,
|
$i_pin,
|
||||||
|
@@ -217,6 +217,7 @@ import * as ep___i_importMuting from './endpoints/i/import-muting.js';
|
|||||||
import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js';
|
import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js';
|
||||||
import * as ep___i_importAntennas from './endpoints/i/import-antennas.js';
|
import * as ep___i_importAntennas from './endpoints/i/import-antennas.js';
|
||||||
import * as ep___i_notifications from './endpoints/i/notifications.js';
|
import * as ep___i_notifications from './endpoints/i/notifications.js';
|
||||||
|
import * as ep___i_notificationsGrouped from './endpoints/i/notifications-grouped.js';
|
||||||
import * as ep___i_pageLikes from './endpoints/i/page-likes.js';
|
import * as ep___i_pageLikes from './endpoints/i/page-likes.js';
|
||||||
import * as ep___i_pages from './endpoints/i/pages.js';
|
import * as ep___i_pages from './endpoints/i/pages.js';
|
||||||
import * as ep___i_pin from './endpoints/i/pin.js';
|
import * as ep___i_pin from './endpoints/i/pin.js';
|
||||||
@@ -572,6 +573,7 @@ const eps = [
|
|||||||
['i/import-user-lists', ep___i_importUserLists],
|
['i/import-user-lists', ep___i_importUserLists],
|
||||||
['i/import-antennas', ep___i_importAntennas],
|
['i/import-antennas', ep___i_importAntennas],
|
||||||
['i/notifications', ep___i_notifications],
|
['i/notifications', ep___i_notifications],
|
||||||
|
['i/notifications-grouped', ep___i_notificationsGrouped],
|
||||||
['i/page-likes', ep___i_pageLikes],
|
['i/page-likes', ep___i_pageLikes],
|
||||||
['i/pages', ep___i_pages],
|
['i/pages', ep___i_pages],
|
||||||
['i/pin', ep___i_pin],
|
['i/pin', ep___i_pin],
|
||||||
|
@@ -11,7 +11,7 @@ export const meta = {
|
|||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireRolePolicy: 'canManageAvatarDecorations',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@@ -13,8 +13,7 @@ export const meta = {
|
|||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireRolePolicy: 'canManageAvatarDecorations',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -16,7 +16,7 @@ export const meta = {
|
|||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireRolePolicy: 'canManageAvatarDecorations',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
@@ -13,7 +13,7 @@ export const meta = {
|
|||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireRolePolicy: 'canManageAvatarDecorations',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
},
|
},
|
||||||
|
@@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Brackets, In } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { NotesRepository } from '@/models/_.js';
|
||||||
|
import { obsoleteNotificationTypes, notificationTypes, FilterUnionByProperty } from '@/types.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { NoteReadService } from '@/core/NoteReadService.js';
|
||||||
|
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { MiGroupedNotification, MiNotification } from '@/models/Notification.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['account', 'notifications'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: 30000,
|
||||||
|
max: 30,
|
||||||
|
},
|
||||||
|
|
||||||
|
kind: 'read:notifications',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'Notification',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
|
markAsRead: { type: 'boolean', default: true },
|
||||||
|
// 後方互換のため、廃止された通知タイプも受け付ける
|
||||||
|
includeTypes: { type: 'array', items: {
|
||||||
|
type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
|
||||||
|
} },
|
||||||
|
excludeTypes: { type: 'array', items: {
|
||||||
|
type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.notesRepository)
|
||||||
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
private idService: IdService,
|
||||||
|
private notificationEntityService: NotificationEntityService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
|
private noteReadService: NoteReadService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const EXTRA_LIMIT = 100;
|
||||||
|
|
||||||
|
// includeTypes が空の場合はクエリしない
|
||||||
|
if (ps.includeTypes && ps.includeTypes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// excludeTypes に全指定されている場合はクエリしない
|
||||||
|
if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
||||||
|
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
||||||
|
|
||||||
|
const limit = (ps.limit + EXTRA_LIMIT) + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||||
|
const notificationsRes = await this.redisClient.xrevrange(
|
||||||
|
`notificationTimeline:${me.id}`,
|
||||||
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
||||||
|
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-',
|
||||||
|
'COUNT', limit);
|
||||||
|
|
||||||
|
if (notificationsRes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[];
|
||||||
|
|
||||||
|
if (includeTypes && includeTypes.length > 0) {
|
||||||
|
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
|
||||||
|
} else if (excludeTypes && excludeTypes.length > 0) {
|
||||||
|
notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifications.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark all as read
|
||||||
|
if (ps.markAsRead) {
|
||||||
|
this.notificationService.readAllNotification(me.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// grouping
|
||||||
|
let groupedNotifications = [notifications[0]] as MiGroupedNotification[];
|
||||||
|
for (let i = 1; i < notifications.length; i++) {
|
||||||
|
const notification = notifications[i];
|
||||||
|
const prev = notifications[i - 1];
|
||||||
|
let prevGroupedNotification = groupedNotifications.at(-1)!;
|
||||||
|
|
||||||
|
if (prev.type === 'reaction' && notification.type === 'reaction' && prev.noteId === notification.noteId) {
|
||||||
|
if (prevGroupedNotification.type !== 'reaction:grouped') {
|
||||||
|
groupedNotifications[groupedNotifications.length - 1] = {
|
||||||
|
type: 'reaction:grouped',
|
||||||
|
id: '',
|
||||||
|
createdAt: prev.createdAt,
|
||||||
|
noteId: prev.noteId!,
|
||||||
|
reactions: [{
|
||||||
|
userId: prev.notifierId!,
|
||||||
|
reaction: prev.reaction!,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
prevGroupedNotification = groupedNotifications.at(-1)!;
|
||||||
|
}
|
||||||
|
(prevGroupedNotification as FilterUnionByProperty<MiGroupedNotification, 'type', 'reaction:grouped'>).reactions.push({
|
||||||
|
userId: notification.notifierId!,
|
||||||
|
reaction: notification.reaction!,
|
||||||
|
});
|
||||||
|
prevGroupedNotification.id = notification.id;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (prev.type === 'renote' && notification.type === 'renote' && prev.targetNoteId === notification.targetNoteId) {
|
||||||
|
if (prevGroupedNotification.type !== 'renote:grouped') {
|
||||||
|
groupedNotifications[groupedNotifications.length - 1] = {
|
||||||
|
type: 'renote:grouped',
|
||||||
|
id: '',
|
||||||
|
createdAt: notification.createdAt,
|
||||||
|
noteId: prev.noteId!,
|
||||||
|
userIds: [prev.notifierId!],
|
||||||
|
};
|
||||||
|
prevGroupedNotification = groupedNotifications.at(-1)!;
|
||||||
|
}
|
||||||
|
(prevGroupedNotification as FilterUnionByProperty<MiGroupedNotification, 'type', 'renote:grouped'>).userIds.push(notification.notifierId!);
|
||||||
|
prevGroupedNotification.id = notification.id;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
groupedNotifications.push(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
groupedNotifications = groupedNotifications.slice(0, ps.limit);
|
||||||
|
|
||||||
|
const noteIds = groupedNotifications
|
||||||
|
.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type))
|
||||||
|
.map(notification => notification.noteId!);
|
||||||
|
|
||||||
|
if (noteIds.length > 0) {
|
||||||
|
const notes = await this.notesRepository.findBy({ id: In(noteIds) });
|
||||||
|
this.noteReadService.read(me.id, notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.notificationEntityService.packGroupedMany(groupedNotifications, me.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -7,7 +7,7 @@ import { Brackets, In } from 'typeorm';
|
|||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository } from '@/models/_.js';
|
import type { NotesRepository } from '@/models/_.js';
|
||||||
import { obsoleteNotificationTypes, notificationTypes } from '@/types.js';
|
import { obsoleteNotificationTypes, notificationTypes, FilterUnionByProperty } from '@/types.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteReadService } from '@/core/NoteReadService.js';
|
import { NoteReadService } from '@/core/NoteReadService.js';
|
||||||
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
||||||
@@ -113,8 +113,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
}
|
}
|
||||||
|
|
||||||
const noteIds = notifications
|
const noteIds = notifications
|
||||||
.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type))
|
.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type))
|
||||||
.map(notification => notification.noteId!);
|
.map(notification => notification.noteId);
|
||||||
|
|
||||||
if (noteIds.length > 0) {
|
if (noteIds.length > 0) {
|
||||||
const notes = await this.notesRepository.findBy({ id: In(noteIds) });
|
const notes = await this.notesRepository.findBy({ id: In(noteIds) });
|
||||||
|
@@ -45,7 +45,7 @@ export const meta = {
|
|||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
duration: ms('1hour'),
|
duration: ms('1hour'),
|
||||||
max: 10,
|
max: 20,
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
@@ -17,6 +17,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
@@ -221,7 +222,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
|
|
||||||
if (renote == null) {
|
if (renote == null) {
|
||||||
throw new ApiError(meta.errors.noSuchRenoteTarget);
|
throw new ApiError(meta.errors.noSuchRenoteTarget);
|
||||||
} else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) {
|
} else if (isPureRenote(renote)) {
|
||||||
throw new ApiError(meta.errors.cannotReRenote);
|
throw new ApiError(meta.errors.cannotReRenote);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +255,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
|
|
||||||
if (reply == null) {
|
if (reply == null) {
|
||||||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||||
} else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) {
|
} else if (isPureRenote(reply)) {
|
||||||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -237,7 +237,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
if (followingChannels.length > 0) {
|
if (followingChannels.length > 0) {
|
||||||
const followingChannelIds = followingChannels.map(x => x.followeeId);
|
const followingChannelIds = followingChannels.map(x => x.followeeId);
|
||||||
|
|
||||||
query.andWhere('note.channelId IN (:...followingChannelIds) OR note.channelId IS NULL', { followingChannelIds });
|
query.andWhere(new Brackets(qb => {
|
||||||
|
qb.where('note.channelId IN (:...followingChannelIds)', { followingChannelIds });
|
||||||
|
qb.orWhere('note.channelId IS NULL');
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
query.andWhere('note.channelId IS NULL');
|
query.andWhere('note.channelId IS NULL');
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { MiNote, NotesRepository } from '@/models/_.js';
|
import type { MiNote, NotesRepository, ChannelFollowingsRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
@@ -58,6 +58,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.channelFollowingsRepository)
|
||||||
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
@@ -160,22 +163,52 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
|
|
||||||
private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; withRenotes: boolean; }, me: MiLocalUser) {
|
private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; withRenotes: boolean; }, me: MiLocalUser) {
|
||||||
const followees = await this.userFollowingService.getFollowees(me.id);
|
const followees = await this.userFollowingService.getFollowees(me.id);
|
||||||
|
const followingChannels = await this.channelFollowingsRepository.find({
|
||||||
|
where: {
|
||||||
|
followerId: me.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
//#region Construct query
|
//#region Construct query
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||||
.andWhere('note.channelId IS NULL')
|
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
.leftJoinAndSelect('note.reply', 'reply')
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
.leftJoinAndSelect('note.renote', 'renote')
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
if (followees.length > 0) {
|
if (followees.length > 0 && followingChannels.length > 0) {
|
||||||
|
// ユーザー・チャンネルともにフォローあり
|
||||||
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
|
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
|
||||||
|
const followingChannelIds = followingChannels.map(x => x.followeeId);
|
||||||
query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
|
query.andWhere(new Brackets(qb => {
|
||||||
|
qb
|
||||||
|
.where(new Brackets(qb2 => {
|
||||||
|
qb2
|
||||||
|
.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds })
|
||||||
|
.andWhere('note.channelId IS NULL');
|
||||||
|
}))
|
||||||
|
.orWhere('note.channelId IN (:...followingChannelIds)', { followingChannelIds });
|
||||||
|
}));
|
||||||
|
} else if (followees.length > 0) {
|
||||||
|
// ユーザーフォローのみ(チャンネルフォローなし)
|
||||||
|
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
|
||||||
|
query
|
||||||
|
.andWhere('note.channelId IS NULL')
|
||||||
|
.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
|
||||||
|
} else if (followingChannels.length > 0) {
|
||||||
|
// チャンネルフォローのみ(ユーザーフォローなし)
|
||||||
|
const followingChannelIds = followingChannels.map(x => x.followeeId);
|
||||||
|
query.andWhere(new Brackets(qb => {
|
||||||
|
qb
|
||||||
|
.where('note.channelId IN (:...followingChannelIds)', { followingChannelIds })
|
||||||
|
.orWhere('note.userId = :meId', { meId: me.id });
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
query.andWhere('note.userId = :meId', { meId: me.id });
|
// フォローなし
|
||||||
|
query
|
||||||
|
.andWhere('note.channelId IS NULL')
|
||||||
|
.andWhere('note.userId = :meId', { meId: me.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
query.andWhere(new Brackets(qb => {
|
query.andWhere(new Brackets(qb => {
|
||||||
|
@@ -42,8 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
this.notificationService.createNotification(user.id, 'app', {
|
this.notificationService.createNotification(user.id, 'app', {
|
||||||
appAccessTokenId: token ? token.id : null,
|
appAccessTokenId: token ? token.id : null,
|
||||||
customBody: ps.body,
|
customBody: ps.body,
|
||||||
customHeader: ps.header,
|
customHeader: ps.header ?? token?.name ?? null,
|
||||||
customIcon: ps.icon,
|
customIcon: ps.icon ?? token?.iconUrl ?? null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -67,6 +67,8 @@ export default abstract class Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public abstract init(params: any): void;
|
public abstract init(params: any): void;
|
||||||
|
|
||||||
public dispose?(): void;
|
public dispose?(): void;
|
||||||
|
|
||||||
public onMessage?(type: string, body: any): void;
|
public onMessage?(type: string, body: any): void;
|
||||||
}
|
}
|
||||||
|
@@ -56,7 +56,7 @@ class HomeTimelineChannel extends Channel {
|
|||||||
if (note.visibility === 'followers') {
|
if (note.visibility === 'followers') {
|
||||||
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
||||||
} else if (note.visibility === 'specified') {
|
} else if (note.visibility === 'specified') {
|
||||||
if (!note.visibleUserIds!.includes(this.user!.id)) return;
|
if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.reply) {
|
if (note.reply) {
|
||||||
|
@@ -67,7 +67,7 @@ class HybridTimelineChannel extends Channel {
|
|||||||
if (note.visibility === 'followers') {
|
if (note.visibility === 'followers') {
|
||||||
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
||||||
} else if (note.visibility === 'specified') {
|
} else if (note.visibility === 'specified') {
|
||||||
if (!note.visibleUserIds!.includes(this.user!.id)) return;
|
if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore notes from instances the user has muted
|
// Ignore notes from instances the user has muted
|
||||||
|
@@ -249,3 +249,9 @@ export type Serialized<T> = {
|
|||||||
? Serialized<T[K]>
|
? Serialized<T[K]>
|
||||||
: T[K];
|
: T[K];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FilterUnionByProperty<
|
||||||
|
Union,
|
||||||
|
Property extends string | number | symbol,
|
||||||
|
Condition
|
||||||
|
> = Union extends Record<Property, Condition> ? Union : never;
|
||||||
|
@@ -720,7 +720,7 @@ describe('クリップ', () => {
|
|||||||
test('を追加できる。', async () => {
|
test('を追加できる。', async () => {
|
||||||
await addNote({ clipId: aliceClip.id, noteId: aliceNote.id });
|
await addNote({ clipId: aliceClip.id, noteId: aliceNote.id });
|
||||||
const res = await show({ clipId: aliceClip.id });
|
const res = await show({ clipId: aliceClip.id });
|
||||||
assert.strictEqual(res.lastClippedAt, new Date(res.lastClippedAt ?? '').toISOString());
|
assert.strictEqual(res.lastClippedAt, res.lastClippedAt ? new Date(res.lastClippedAt).toISOString() : null);
|
||||||
assert.deepStrictEqual((await notes({ clipId: aliceClip.id })).map(x => x.id), [aliceNote.id]);
|
assert.deepStrictEqual((await notes({ clipId: aliceClip.id })).map(x => x.id), [aliceNote.id]);
|
||||||
|
|
||||||
// 他人の非公開ノートも突っ込める
|
// 他人の非公開ノートも突っ込める
|
||||||
|
@@ -164,6 +164,7 @@ describe('ユーザー', () => {
|
|||||||
hasUnreadAntenna: user.hasUnreadAntenna,
|
hasUnreadAntenna: user.hasUnreadAntenna,
|
||||||
hasUnreadChannel: user.hasUnreadChannel,
|
hasUnreadChannel: user.hasUnreadChannel,
|
||||||
hasUnreadNotification: user.hasUnreadNotification,
|
hasUnreadNotification: user.hasUnreadNotification,
|
||||||
|
unreadNotificationsCount: user.unreadNotificationsCount,
|
||||||
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
|
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
|
||||||
unreadAnnouncements: user.unreadAnnouncements,
|
unreadAnnouncements: user.unreadAnnouncements,
|
||||||
mutedWords: user.mutedWords,
|
mutedWords: user.mutedWords,
|
||||||
@@ -414,6 +415,7 @@ describe('ユーザー', () => {
|
|||||||
assert.strictEqual(response.hasUnreadAntenna, false);
|
assert.strictEqual(response.hasUnreadAntenna, false);
|
||||||
assert.strictEqual(response.hasUnreadChannel, false);
|
assert.strictEqual(response.hasUnreadChannel, false);
|
||||||
assert.strictEqual(response.hasUnreadNotification, false);
|
assert.strictEqual(response.hasUnreadNotification, false);
|
||||||
|
assert.strictEqual(response.unreadNotificationsCount, 0);
|
||||||
assert.strictEqual(response.hasPendingReceivedFollowRequest, false);
|
assert.strictEqual(response.hasPendingReceivedFollowRequest, false);
|
||||||
assert.deepStrictEqual(response.unreadAnnouncements, []);
|
assert.deepStrictEqual(response.unreadAnnouncements, []);
|
||||||
assert.deepStrictEqual(response.mutedWords, []);
|
assert.deepStrictEqual(response.mutedWords, []);
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
"build-storybook": "pnpm build-storybook-pre && storybook build",
|
"build-storybook": "pnpm build-storybook-pre && storybook build",
|
||||||
"chromatic": "chromatic",
|
"chromatic": "chromatic",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
"test-and-coverage": "vitest --run --coverage",
|
"test-and-coverage": "vitest --run --coverage --globals",
|
||||||
"typecheck": "vue-tsc --noEmit",
|
"typecheck": "vue-tsc --noEmit",
|
||||||
"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",
|
"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",
|
||||||
"lint": "pnpm typecheck && pnpm eslint"
|
"lint": "pnpm typecheck && pnpm eslint"
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"@github/webauthn-json": "2.1.1",
|
"@github/webauthn-json": "2.1.1",
|
||||||
"@rollup/plugin-alias": "5.0.1",
|
"@rollup/plugin-alias": "5.0.1",
|
||||||
"@rollup/plugin-json": "6.0.1",
|
"@rollup/plugin-json": "6.0.1",
|
||||||
"@rollup/plugin-replace": "5.0.4",
|
"@rollup/plugin-replace": "5.0.5",
|
||||||
"@rollup/pluginutils": "5.0.5",
|
"@rollup/pluginutils": "5.0.5",
|
||||||
"@syuilo/aiscript": "0.16.0",
|
"@syuilo/aiscript": "0.16.0",
|
||||||
"@tabler/icons-webfont": "2.37.0",
|
"@tabler/icons-webfont": "2.37.0",
|
||||||
@@ -29,7 +29,8 @@
|
|||||||
"@vue/compiler-sfc": "3.3.7",
|
"@vue/compiler-sfc": "3.3.7",
|
||||||
"astring": "1.8.6",
|
"astring": "1.8.6",
|
||||||
"autosize": "6.0.1",
|
"autosize": "6.0.1",
|
||||||
"broadcast-channel": "5.5.1",
|
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.5",
|
||||||
|
"broadcast-channel": "6.0.0",
|
||||||
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||||
"buraha": "0.0.1",
|
"buraha": "0.0.1",
|
||||||
"canvas-confetti": "1.6.1",
|
"canvas-confetti": "1.6.1",
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
"chartjs-chart-matrix": "2.0.1",
|
"chartjs-chart-matrix": "2.0.1",
|
||||||
"chartjs-plugin-gradient": "0.6.1",
|
"chartjs-plugin-gradient": "0.6.1",
|
||||||
"chartjs-plugin-zoom": "2.0.1",
|
"chartjs-plugin-zoom": "2.0.1",
|
||||||
"chromatic": "7.5.4",
|
"chromatic": "7.6.0",
|
||||||
"compare-versions": "6.1.0",
|
"compare-versions": "6.1.0",
|
||||||
"cropperjs": "2.0.0-beta.4",
|
"cropperjs": "2.0.0-beta.4",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
@@ -54,11 +55,11 @@
|
|||||||
"mfm-js": "0.23.3",
|
"mfm-js": "0.23.3",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
"photoswipe": "5.4.2",
|
"photoswipe": "5.4.2",
|
||||||
"prismjs": "1.29.0",
|
"punycode": "2.3.1",
|
||||||
"punycode": "2.3.0",
|
|
||||||
"querystring": "0.2.1",
|
"querystring": "0.2.1",
|
||||||
"rollup": "4.1.4",
|
"rollup": "4.2.0",
|
||||||
"sanitize-html": "2.11.0",
|
"sanitize-html": "2.11.0",
|
||||||
|
"shiki": "^0.14.5",
|
||||||
"sass": "1.69.5",
|
"sass": "1.69.5",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
@@ -74,34 +75,33 @@
|
|||||||
"vanilla-tilt": "1.8.1",
|
"vanilla-tilt": "1.8.1",
|
||||||
"vite": "4.5.0",
|
"vite": "4.5.0",
|
||||||
"vue": "3.3.7",
|
"vue": "3.3.7",
|
||||||
"vue-prism-editor": "2.0.0-alpha.2",
|
|
||||||
"vuedraggable": "next"
|
"vuedraggable": "next"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-actions": "7.5.1",
|
"@storybook/addon-actions": "7.5.2",
|
||||||
"@storybook/addon-essentials": "7.5.1",
|
"@storybook/addon-essentials": "7.5.2",
|
||||||
"@storybook/addon-interactions": "7.5.1",
|
"@storybook/addon-interactions": "7.5.2",
|
||||||
"@storybook/addon-links": "7.5.1",
|
"@storybook/addon-links": "7.5.2",
|
||||||
"@storybook/addon-storysource": "7.5.1",
|
"@storybook/addon-storysource": "7.5.2",
|
||||||
"@storybook/addons": "7.5.1",
|
"@storybook/addons": "7.5.2",
|
||||||
"@storybook/blocks": "7.5.1",
|
"@storybook/blocks": "7.5.2",
|
||||||
"@storybook/core-events": "7.5.1",
|
"@storybook/core-events": "7.5.2",
|
||||||
"@storybook/jest": "0.2.3",
|
"@storybook/jest": "0.2.3",
|
||||||
"@storybook/manager-api": "7.5.1",
|
"@storybook/manager-api": "7.5.2",
|
||||||
"@storybook/preview-api": "7.5.1",
|
"@storybook/preview-api": "7.5.2",
|
||||||
"@storybook/react": "7.5.1",
|
"@storybook/react": "7.5.2",
|
||||||
"@storybook/react-vite": "7.5.1",
|
"@storybook/react-vite": "7.5.2",
|
||||||
"@storybook/testing-library": "0.2.2",
|
"@storybook/testing-library": "0.2.2",
|
||||||
"@storybook/theming": "7.5.1",
|
"@storybook/theming": "7.5.2",
|
||||||
"@storybook/types": "7.5.1",
|
"@storybook/types": "7.5.2",
|
||||||
"@storybook/vue3": "7.5.1",
|
"@storybook/vue3": "7.5.2",
|
||||||
"@storybook/vue3-vite": "7.5.1",
|
"@storybook/vue3-vite": "7.5.2",
|
||||||
"@testing-library/vue": "7.0.0",
|
"@testing-library/vue": "8.0.0",
|
||||||
"@types/escape-regexp": "0.0.2",
|
"@types/escape-regexp": "0.0.2",
|
||||||
"@types/estree": "1.0.3",
|
"@types/estree": "1.0.4",
|
||||||
"@types/matter-js": "0.19.2",
|
"@types/matter-js": "0.19.2",
|
||||||
"@types/micromatch": "4.0.4",
|
"@types/micromatch": "4.0.4",
|
||||||
"@types/node": "20.8.9",
|
"@types/node": "20.8.10",
|
||||||
"@types/punycode": "2.1.1",
|
"@types/punycode": "2.1.1",
|
||||||
"@types/sanitize-html": "2.9.3",
|
"@types/sanitize-html": "2.9.3",
|
||||||
"@types/throttle-debounce": "5.0.1",
|
"@types/throttle-debounce": "5.0.1",
|
||||||
@@ -109,13 +109,13 @@
|
|||||||
"@types/uuid": "9.0.6",
|
"@types/uuid": "9.0.6",
|
||||||
"@types/websocket": "1.0.8",
|
"@types/websocket": "1.0.8",
|
||||||
"@types/ws": "8.5.8",
|
"@types/ws": "8.5.8",
|
||||||
"@typescript-eslint/eslint-plugin": "6.9.0",
|
"@typescript-eslint/eslint-plugin": "6.9.1",
|
||||||
"@typescript-eslint/parser": "6.9.0",
|
"@typescript-eslint/parser": "6.9.1",
|
||||||
"@vitest/coverage-v8": "0.34.6",
|
"@vitest/coverage-v8": "0.34.6",
|
||||||
"@vue/runtime-core": "3.3.7",
|
"@vue/runtime-core": "3.3.7",
|
||||||
"acorn": "8.11.2",
|
"acorn": "8.11.2",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.3.3",
|
"cypress": "13.4.0",
|
||||||
"eslint": "8.52.0",
|
"eslint": "8.52.0",
|
||||||
"eslint-plugin-import": "2.29.0",
|
"eslint-plugin-import": "2.29.0",
|
||||||
"eslint-plugin-vue": "9.18.1",
|
"eslint-plugin-vue": "9.18.1",
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"start-server-and-test": "2.0.1",
|
"start-server-and-test": "2.0.1",
|
||||||
"storybook": "7.5.1",
|
"storybook": "7.5.2",
|
||||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||||
"summaly": "github:misskey-dev/summaly",
|
"summaly": "github:misskey-dev/summaly",
|
||||||
"vite-plugin-turbosnap": "1.0.3",
|
"vite-plugin-turbosnap": "1.0.3",
|
||||||
|
@@ -8,7 +8,7 @@ import { common } from './common.js';
|
|||||||
import { version, ui, lang, updateLocale } from '@/config.js';
|
import { version, ui, lang, updateLocale } from '@/config.js';
|
||||||
import { i18n, updateI18n } from '@/i18n.js';
|
import { i18n, updateI18n } from '@/i18n.js';
|
||||||
import { confirm, alert, post, popup, toast } from '@/os.js';
|
import { confirm, alert, post, popup, toast } from '@/os.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream, isReloading } from '@/stream.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js';
|
import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js';
|
||||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||||
@@ -39,6 +39,7 @@ export async function mainBoot() {
|
|||||||
|
|
||||||
let reloadDialogShowing = false;
|
let reloadDialogShowing = false;
|
||||||
stream.on('_disconnected_', async () => {
|
stream.on('_disconnected_', async () => {
|
||||||
|
if (isReloading) return;
|
||||||
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
|
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
|
||||||
location.reload();
|
location.reload();
|
||||||
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
|
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
|
||||||
@@ -225,11 +226,18 @@ export async function mainBoot() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
main.on('readAllNotifications', () => {
|
main.on('readAllNotifications', () => {
|
||||||
updateAccount({ hasUnreadNotification: false });
|
updateAccount({
|
||||||
|
hasUnreadNotification: false,
|
||||||
|
unreadNotificationsCount: 0,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
main.on('unreadNotification', () => {
|
main.on('unreadNotification', () => {
|
||||||
updateAccount({ hasUnreadNotification: true });
|
const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
|
||||||
|
updateAccount({
|
||||||
|
hasUnreadNotification: true,
|
||||||
|
unreadNotificationsCount,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
main.on('unreadMention', () => {
|
main.on('unreadMention', () => {
|
||||||
|
@@ -5,21 +5,90 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<code v-if="inline" :class="`language-${prismLang}`" style="overflow-wrap: anywhere;" v-html="html"></code>
|
<div :class="['codeBlockRoot', { 'codeEditor': codeEditor }]" v-html="html"></div>
|
||||||
<pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import Prism from 'prismjs';
|
import { BUNDLED_LANGUAGES } from 'shiki';
|
||||||
import 'prismjs/themes/prism-okaidia.css';
|
import type { Lang as ShikiLang } from 'shiki';
|
||||||
|
import { getHighlighter } from '@/scripts/code-highlighter.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
code: string;
|
code: string;
|
||||||
lang?: string;
|
lang?: string;
|
||||||
inline?: boolean;
|
codeEditor?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const prismLang = computed(() => Prism.languages[props.lang] ? props.lang : 'js');
|
const highlighter = await getHighlighter();
|
||||||
const html = computed(() => Prism.highlight(props.code, Prism.languages[prismLang.value], prismLang.value));
|
|
||||||
|
const codeLang = ref<ShikiLang | 'aiscript'>('js');
|
||||||
|
const html = computed(() => highlighter.codeToHtml(props.code, {
|
||||||
|
lang: codeLang.value,
|
||||||
|
theme: 'dark-plus',
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function fetchLanguage(to: string): Promise<void> {
|
||||||
|
const language = to as ShikiLang;
|
||||||
|
|
||||||
|
// Check for the loaded languages, and load the language if it's not loaded yet.
|
||||||
|
if (!highlighter.getLoadedLanguages().includes(language)) {
|
||||||
|
// Check if the language is supported by Shiki
|
||||||
|
const bundles = BUNDLED_LANGUAGES.filter((bundle) => {
|
||||||
|
// Languages are specified by their id, they can also have aliases (i. e. "js" and "javascript")
|
||||||
|
return bundle.id === language || bundle.aliases?.includes(language);
|
||||||
|
});
|
||||||
|
if (bundles.length > 0) {
|
||||||
|
await highlighter.loadLanguage(language);
|
||||||
|
codeLang.value = language;
|
||||||
|
} else {
|
||||||
|
codeLang.value = 'js';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
codeLang.value = language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.lang, (to) => {
|
||||||
|
if (codeLang.value === to || !to) return;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
fetchLanguage(to).then(() => resolve);
|
||||||
|
});
|
||||||
|
}, { immediate: true, });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.codeBlockRoot :deep(.shiki) {
|
||||||
|
padding: 1em;
|
||||||
|
margin: .5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: .3em;
|
||||||
|
|
||||||
|
& pre,
|
||||||
|
& code {
|
||||||
|
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlockRoot.codeEditor {
|
||||||
|
min-width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
& :deep(.shiki) {
|
||||||
|
padding: 12px;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
min-height: 130px;
|
||||||
|
pointer-events: none;
|
||||||
|
min-width: calc(100% - 24px);
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1.5em;
|
||||||
|
font-size: 1em;
|
||||||
|
overflow: visible;
|
||||||
|
text-rendering: inherit;
|
||||||
|
text-transform: inherit;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@@ -4,11 +4,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<XCode :code="code" :lang="lang" :inline="inline"/>
|
<Suspense>
|
||||||
|
<template #fallback>
|
||||||
|
<MkLoading v-if="!inline ?? true" />
|
||||||
|
</template>
|
||||||
|
<code v-if="inline" :class="$style.codeInlineRoot">{{ code }}</code>
|
||||||
|
<XCode v-else :code="code" :lang="lang"/>
|
||||||
|
</Suspense>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import MkLoading from '@/components/global/MkLoading.vue';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
code: string;
|
code: string;
|
||||||
@@ -18,3 +25,15 @@ defineProps<{
|
|||||||
|
|
||||||
const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
|
const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.codeInlineRoot {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: #D4D4D4;
|
||||||
|
background: #1E1E1E;
|
||||||
|
padding: .1em;
|
||||||
|
border-radius: .3em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
166
packages/frontend/src/components/MkCodeEditor.vue
Normal file
166
packages/frontend/src/components/MkCodeEditor.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="[$style.codeEditorRoot, { [$style.disabled]: disabled, [$style.focused]: focused }]">
|
||||||
|
<div :class="$style.codeEditorScroller">
|
||||||
|
<textarea
|
||||||
|
ref="inputEl"
|
||||||
|
v-model="vModel"
|
||||||
|
:class="[$style.textarea]"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="required"
|
||||||
|
:readonly="readonly"
|
||||||
|
autocomplete="off"
|
||||||
|
wrap="off"
|
||||||
|
spellcheck="false"
|
||||||
|
@focus="focused = true"
|
||||||
|
@blur="focused = false"
|
||||||
|
@keydown="onKeydown($event)"
|
||||||
|
@input="onInput"
|
||||||
|
></textarea>
|
||||||
|
<XCode :class="$style.codeEditorHighlighter" :codeEditor="true" :code="v" :lang="lang"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch, toRefs, shallowRef, nextTick } from 'vue';
|
||||||
|
import XCode from '@/components/MkCode.core.vue';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
modelValue: string | null;
|
||||||
|
lang: string;
|
||||||
|
required?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}>(), {
|
||||||
|
lang: 'js',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'change', _ev: KeyboardEvent): void;
|
||||||
|
(ev: 'keydown', _ev: KeyboardEvent): void;
|
||||||
|
(ev: 'enter'): void;
|
||||||
|
(ev: 'update:modelValue', value: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { modelValue } = toRefs(props);
|
||||||
|
const vModel = ref<string>(modelValue.value ?? '');
|
||||||
|
const v = ref<string>(modelValue.value ?? '');
|
||||||
|
const focused = ref(false);
|
||||||
|
const changed = ref(false);
|
||||||
|
const inputEl = shallowRef<HTMLTextAreaElement>();
|
||||||
|
|
||||||
|
const onInput = (ev) => {
|
||||||
|
v.value = ev.target?.value ?? v.value;
|
||||||
|
changed.value = true;
|
||||||
|
emit('change', ev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeydown = (ev: KeyboardEvent) => {
|
||||||
|
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
|
||||||
|
|
||||||
|
emit('keydown', ev);
|
||||||
|
|
||||||
|
if (ev.code === 'Enter') {
|
||||||
|
const pos = inputEl.value?.selectionStart ?? 0;
|
||||||
|
const posEnd = inputEl.value?.selectionEnd ?? vModel.value.length;
|
||||||
|
if (pos === posEnd) {
|
||||||
|
const lines = vModel.value.slice(0, pos).split('\n');
|
||||||
|
const currentLine = lines[lines.length - 1];
|
||||||
|
const currentLineSpaces = currentLine.match(/^\s+/);
|
||||||
|
const posDelta = currentLineSpaces ? currentLineSpaces[0].length : 0;
|
||||||
|
ev.preventDefault();
|
||||||
|
vModel.value = vModel.value.slice(0, pos) + '\n' + (currentLineSpaces ? currentLineSpaces[0] : '') + vModel.value.slice(pos);
|
||||||
|
v.value = vModel.value;
|
||||||
|
nextTick(() => {
|
||||||
|
inputEl.value?.setSelectionRange(pos + 1 + posDelta, pos + 1 + posDelta);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
emit('enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.key === 'Tab') {
|
||||||
|
const pos = inputEl.value?.selectionStart ?? 0;
|
||||||
|
const posEnd = inputEl.value?.selectionEnd ?? vModel.value.length;
|
||||||
|
vModel.value = vModel.value.slice(0, pos) + '\t' + vModel.value.slice(posEnd);
|
||||||
|
v.value = vModel.value;
|
||||||
|
nextTick(() => {
|
||||||
|
inputEl.value?.setSelectionRange(pos + 1, pos + 1);
|
||||||
|
});
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updated = () => {
|
||||||
|
changed.value = false;
|
||||||
|
emit('update:modelValue', v.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(modelValue, newValue => {
|
||||||
|
v.value = newValue ?? '';
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(v, () => {
|
||||||
|
updated();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.codeEditorRoot {
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--fg);
|
||||||
|
border: solid 1px var(--panel);
|
||||||
|
transition: border-color 0.1s ease-out;
|
||||||
|
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--inputBorderHover) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.focused.codeEditorRoot {
|
||||||
|
border-color: var(--accent) !important;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeEditorScroller {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: inline-block;
|
||||||
|
appearance: none;
|
||||||
|
resize: none;
|
||||||
|
text-align: left;
|
||||||
|
color: transparent;
|
||||||
|
caret-color: rgb(225, 228, 232);
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
padding: 12px;
|
||||||
|
line-height: 1.5em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea::selection {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -42,6 +42,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
setup(props, { slots, expose }) {
|
setup(props, { slots, expose }) {
|
||||||
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
|
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
|
||||||
|
|
||||||
function getDateText(time: string) {
|
function getDateText(time: string) {
|
||||||
const date = new Date(time).getDate();
|
const date = new Date(time).getDate();
|
||||||
const month = new Date(time).getMonth() + 1;
|
const month = new Date(time).getMonth() + 1;
|
||||||
@@ -121,6 +122,7 @@ export default defineComponent({
|
|||||||
el.style.top = `${el.offsetTop}px`;
|
el.style.top = `${el.offsetTop}px`;
|
||||||
el.style.left = `${el.offsetLeft}px`;
|
el.style.left = `${el.offsetLeft}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onLeaveCanceled(el: HTMLElement) {
|
function onLeaveCanceled(el: HTMLElement) {
|
||||||
el.style.top = '';
|
el.style.top = '';
|
||||||
el.style.left = '';
|
el.style.left = '';
|
||||||
|
@@ -160,6 +160,7 @@ async function ok() {
|
|||||||
function cancel() {
|
function cancel() {
|
||||||
done(true);
|
done(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
function onBgClick() {
|
function onBgClick() {
|
||||||
if (props.cancelableByBgClick) cancel();
|
if (props.cancelableByBgClick) cancel();
|
||||||
|
@@ -505,6 +505,7 @@ function appendFile(file: Misskey.entities.DriveFile) {
|
|||||||
function appendFolder(folderToAppend: Misskey.entities.DriveFolder) {
|
function appendFolder(folderToAppend: Misskey.entities.DriveFolder) {
|
||||||
addFolder(folderToAppend);
|
addFolder(folderToAppend);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
function prependFile(file: Misskey.entities.DriveFile) {
|
function prependFile(file: Misskey.entities.DriveFile) {
|
||||||
addFile(file, true);
|
addFile(file, true);
|
||||||
|
@@ -84,6 +84,7 @@ onMounted(() => {
|
|||||||
return getParentBg(el.parentElement);
|
return getParentBg(el.parentElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawBg = getParentBg(el.value);
|
const rawBg = getParentBg(el.value);
|
||||||
const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||||
_bg.setAlpha(0.85);
|
_bg.setAlpha(0.85);
|
||||||
|
@@ -7,16 +7,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<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')">
|
||||||
<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">
|
<template v-for="item in items" :key="item.text">
|
||||||
<button v-if="item.action" v-click-anime class="_button item" @click="$event => { item.action($event); close(); }">
|
<button v-if="item.action" v-click-anime class="_button item" @click="$event => { item.action($event); close(); }">
|
||||||
<i class="icon" :class="item.icon"></i>
|
<i class="icon" :class="item.icon"></i>
|
||||||
<div class="text">{{ item.text }}</div>
|
<div class="text">{{ item.text }}</div>
|
||||||
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
|
<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
|
||||||
|
<span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<MkA v-else v-click-anime :to="item.to" class="item" @click.passive="close()">
|
<MkA v-else v-click-anime :to="item.to" class="item" @click.passive="close()">
|
||||||
<i class="icon" :class="item.icon"></i>
|
<i class="icon" :class="item.icon"></i>
|
||||||
<div class="text">{{ item.text }}</div>
|
<div class="text">{{ item.text }}</div>
|
||||||
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
|
<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
|
||||||
|
<span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import { navbarItemDef } from '@/navbar';
|
import { navbarItemDef } from '@/navbar.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';
|
||||||
|
|
||||||
@@ -57,6 +59,7 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k =>
|
|||||||
to: def.to,
|
to: def.to,
|
||||||
action: def.action,
|
action: def.action,
|
||||||
indicate: def.indicated,
|
indicate: def.indicated,
|
||||||
|
indicateValue: def.indicateValue,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
@@ -116,6 +119,17 @@ function close() {
|
|||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .indicatorWithValue {
|
||||||
|
position: absolute;
|
||||||
|
top: 32px;
|
||||||
|
left: 16px;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
top: 16px;
|
||||||
|
left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .indicator {
|
> .indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 32px;
|
top: 32px;
|
||||||
|
@@ -145,11 +145,13 @@ const onGlobalMousedown = (event: MouseEvent) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let childCloseTimer: null | number = null;
|
let childCloseTimer: null | number = null;
|
||||||
|
|
||||||
function onItemMouseEnter(item) {
|
function onItemMouseEnter(item) {
|
||||||
childCloseTimer = window.setTimeout(() => {
|
childCloseTimer = window.setTimeout(() => {
|
||||||
closeChild();
|
closeChild();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onItemMouseLeave(item) {
|
function onItemMouseLeave(item) {
|
||||||
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
||||||
}
|
}
|
||||||
|
@@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div :class="$style.text">
|
<div :class="$style.text">
|
||||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||||
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
|
<Mfm v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||||
<div v-if="translating || translation" :class="$style.translation">
|
<div v-if="translating || translation" :class="$style.translation">
|
||||||
<MkLoading v-if="translating" mini/>
|
<MkLoading v-if="translating" mini/>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
@@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
<MkReactionsViewer :note="appearNote" :maxNumber="16">
|
<MkReactionsViewer v-show="appearNote.cw == null || showContent" :note="appearNote" :maxNumber="16">
|
||||||
<template #more>
|
<template #more>
|
||||||
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
|
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -209,8 +209,9 @@ const clipButton = shallowRef<HTMLElement>();
|
|||||||
let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
|
let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
|
||||||
const isMyRenote = $i && ($i.id === note.userId);
|
const isMyRenote = $i && ($i.id === note.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
const parsed = appearNote.text ? mfm.parse(appearNote.text) : null;
|
||||||
const isLong = shouldCollapsed(appearNote);
|
const urls = parsed ? extractUrlFromMfm(parsed) : null;
|
||||||
|
const isLong = shouldCollapsed(appearNote, urls ?? []);
|
||||||
const collapsed = ref(appearNote.cw == null && isLong);
|
const collapsed = ref(appearNote.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
||||||
|
@@ -73,7 +73,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div v-show="appearNote.cw == null || showContent">
|
<div v-show="appearNote.cw == null || showContent">
|
||||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||||
<MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
<MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
|
<Mfm v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||||
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
|
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
|
||||||
<div v-if="translating || translation" :class="$style.translation">
|
<div v-if="translating || translation" :class="$style.translation">
|
||||||
<MkLoading v-if="translating" mini/>
|
<MkLoading v-if="translating" mini/>
|
||||||
@@ -260,7 +260,8 @@ const isDeleted = ref(false);
|
|||||||
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
const parsed = appearNote.text ? mfm.parse(appearNote.text) : null;
|
||||||
|
const urls = parsed ? extractUrlFromMfm(parsed) : null;
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||||
const conversation = ref<Misskey.entities.Note[]>([]);
|
const conversation = ref<Misskey.entities.Note[]>([]);
|
||||||
const replies = ref<Misskey.entities.Note[]>([]);
|
const replies = ref<Misskey.entities.Note[]>([]);
|
||||||
@@ -499,6 +500,7 @@ function blur() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const repliesLoaded = ref(false);
|
const repliesLoaded = ref(false);
|
||||||
|
|
||||||
function loadReplies() {
|
function loadReplies() {
|
||||||
repliesLoaded.value = true;
|
repliesLoaded.value = true;
|
||||||
os.api('notes/children', {
|
os.api('notes/children', {
|
||||||
@@ -510,6 +512,7 @@ function loadReplies() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const conversationLoaded = ref(false);
|
const conversationLoaded = ref(false);
|
||||||
|
|
||||||
function loadConversation() {
|
function loadConversation() {
|
||||||
conversationLoaded.value = true;
|
conversationLoaded.value = true;
|
||||||
os.api('notes/conversation', {
|
os.api('notes/conversation', {
|
||||||
|
@@ -9,9 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
|
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||||
<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
|
<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||||
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
|
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
|
||||||
|
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
||||||
|
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||||
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
|
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
|
||||||
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
|
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
|
||||||
<img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/>
|
<img v-else-if="notification.icon" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
|
||||||
<div
|
<div
|
||||||
:class="[$style.subIcon, {
|
:class="[$style.subIcon, {
|
||||||
[$style.t_follow]: notification.type === 'follow',
|
[$style.t_follow]: notification.type === 'follow',
|
||||||
@@ -39,7 +41,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
v-else-if="notification.type === 'reaction'"
|
v-else-if="notification.type === 'reaction'"
|
||||||
ref="reactionRef"
|
ref="reactionRef"
|
||||||
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
||||||
:customEmojis="notification.note.emojis"
|
|
||||||
:noStyle="true"
|
:noStyle="true"
|
||||||
style="width: 100%; height: 100%;"
|
style="width: 100%; height: 100%;"
|
||||||
/>
|
/>
|
||||||
@@ -52,16 +53,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||||
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||||
|
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.t('_notification.reactedBySomeUsers', { n: notification.reactions.length }) }}</span>
|
||||||
|
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.t('_notification.renotedBySomeUsers', { n: notification.users.length }) }}</span>
|
||||||
<span v-else>{{ notification.header }}</span>
|
<span v-else>{{ notification.header }}</span>
|
||||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div>
|
||||||
<MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA v-if="notification.type === 'reaction' || notification.type === 'reaction:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-else-if="notification.type === 'renote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
<MkA v-else-if="notification.type === 'renote' || notification.type === 'renote:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
||||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :author="notification.note.renote.user"/>
|
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :author="notification.note.renote.user"/>
|
||||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
@@ -102,6 +105,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<span v-else-if="notification.type === 'app'" :class="$style.text">
|
<span v-else-if="notification.type === 'app'" :class="$style.text">
|
||||||
<Mfm :text="notification.body" :nowrap="false"/>
|
<Mfm :text="notification.body" :nowrap="false"/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<div v-if="notification.type === 'reaction:grouped'">
|
||||||
|
<div v-for="reaction of notification.reactions" :class="$style.reactionsItem">
|
||||||
|
<MkAvatar :class="$style.reactionsItemAvatar" :user="reaction.user" link preview/>
|
||||||
|
<div :class="$style.reactionsItemReaction">
|
||||||
|
<MkReactionIcon
|
||||||
|
:reaction="reaction.reaction ? reaction.reaction.replace(/^:(\w+):$/, ':$1@.:') : reaction.reaction"
|
||||||
|
:noStyle="true"
|
||||||
|
style="width: 100%; height: 100%;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="notification.type === 'renote:grouped'">
|
||||||
|
<div v-for="user of notification.users" :class="$style.reactionsItem">
|
||||||
|
<MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,6 +202,29 @@ useTooltip(reactionRef, (showing) => {
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon_reactionGroup,
|
||||||
|
.icon_renoteGroup {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
font-size: 15px;
|
||||||
|
border-radius: 100%;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon_reactionGroup {
|
||||||
|
background: #e99a0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon_renoteGroup {
|
||||||
|
background: #36d298;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon_app {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,6 +349,36 @@ useTooltip(reactionRef, (showing) => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reactionsItem {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactionsItemAvatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactionsItemReaction {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
bottom: -2px;
|
||||||
|
right: -2px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 100%;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 0 0 3px var(--panel);
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
@container (max-width: 600px) {
|
@container (max-width: 600px) {
|
||||||
.root {
|
.root {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
@@ -15,14 +15,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #default="{ items: notifications }">
|
<template #default="{ items: notifications }">
|
||||||
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
|
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
|
||||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
||||||
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel notification"/>
|
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
|
||||||
</MkDateSeparatedList>
|
</MkDateSeparatedList>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onUnmounted, onMounted, computed, shallowRef } from 'vue';
|
import { onUnmounted, onDeactivated, onMounted, computed, shallowRef } from 'vue';
|
||||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||||
import XNotification from '@/components/MkNotification.vue';
|
import XNotification from '@/components/MkNotification.vue';
|
||||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||||
@@ -32,6 +32,7 @@ import { $i } from '@/account.js';
|
|||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { notificationTypes } from '@/const.js';
|
import { notificationTypes } from '@/const.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
excludeTypes?: typeof notificationTypes[number][];
|
excludeTypes?: typeof notificationTypes[number][];
|
||||||
@@ -39,7 +40,13 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
const pagination: Paging = {
|
const pagination: Paging = defaultStore.state.useGroupedNotifications ? {
|
||||||
|
endpoint: 'i/notifications-grouped' as const,
|
||||||
|
limit: 20,
|
||||||
|
params: computed(() => ({
|
||||||
|
excludeTypes: props.excludeTypes ?? undefined,
|
||||||
|
})),
|
||||||
|
} : {
|
||||||
endpoint: 'i/notifications' as const,
|
endpoint: 'i/notifications' as const,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
params: computed(() => ({
|
params: computed(() => ({
|
||||||
@@ -68,6 +75,10 @@ onMounted(() => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (connection) connection.dispose();
|
if (connection) connection.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
if (connection) connection.dispose();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@@ -166,6 +166,8 @@ defineExpose({
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.root {
|
||||||
|
overscroll-behavior: none;
|
||||||
|
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
|
||||||
|
@@ -87,6 +87,7 @@ function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] {
|
|||||||
function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): MisskeyEntityMap {
|
function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): MisskeyEntityMap {
|
||||||
return new Map([...map, ...arrayToEntries(entities)]);
|
return new Map([...map, ...arrayToEntries(entities)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
@@ -101,6 +102,7 @@ const props = withDefaults(defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'queue', count: number): void;
|
(ev: 'queue', count: number): void;
|
||||||
|
(ev: 'status', error: boolean): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let rootEl = $shallowRef<HTMLElement>();
|
let rootEl = $shallowRef<HTMLElement>();
|
||||||
@@ -192,6 +194,11 @@ watch(queue, (a, b) => {
|
|||||||
emit('queue', queue.value.size);
|
emit('queue', queue.value.size);
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
|
watch(error, (n, o) => {
|
||||||
|
if (n === o) return;
|
||||||
|
emit('status', n);
|
||||||
|
});
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<void> {
|
||||||
items.value = new Map();
|
items.value = new Map();
|
||||||
queue.value = new Map();
|
queue.value = new Map();
|
||||||
|
@@ -59,6 +59,7 @@ function toggleSensitive(file) {
|
|||||||
emit('changeSensitive', file, !file.isSensitive);
|
emit('changeSensitive', file, !file.isSensitive);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rename(file) {
|
async function rename(file) {
|
||||||
const { canceled, result } = await os.inputText({
|
const { canceled, result } = await os.inputText({
|
||||||
title: i18n.ts.enterFileName,
|
title: i18n.ts.enterFileName,
|
||||||
|
240
packages/frontend/src/components/MkPullToRefresh.vue
Normal file
240
packages/frontend/src/components/MkPullToRefresh.vue
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="rootEl">
|
||||||
|
<div v-if="isPullStart" :class="$style.frame" :style="`--frame-min-height: ${pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR))}px;`">
|
||||||
|
<div :class="$style.frameContent">
|
||||||
|
<MkLoading v-if="isRefreshing" :class="$style.loader" :em="true"/>
|
||||||
|
<i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPullEnd }]"></i>
|
||||||
|
<div :class="$style.text">
|
||||||
|
<template v-if="isPullEnd">{{ i18n.ts.releaseToRefresh }}</template>
|
||||||
|
<template v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</template>
|
||||||
|
<template v-else>{{ i18n.ts.pullDownToRefresh }}</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="{ [$style.slotClip]: isPullStart }">
|
||||||
|
<slot/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, onUnmounted } from 'vue';
|
||||||
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
const SCROLL_STOP = 10;
|
||||||
|
const MAX_PULL_DISTANCE = Infinity;
|
||||||
|
const FIRE_THRESHOLD = 230;
|
||||||
|
const RELEASE_TRANSITION_DURATION = 200;
|
||||||
|
const PULL_BRAKE_BASE = 2;
|
||||||
|
const PULL_BRAKE_FACTOR = 200;
|
||||||
|
|
||||||
|
let isPullStart = $ref(false);
|
||||||
|
let isPullEnd = $ref(false);
|
||||||
|
let isRefreshing = $ref(false);
|
||||||
|
let pullDistance = $ref(0);
|
||||||
|
|
||||||
|
let supportPointerDesktop = false;
|
||||||
|
let startScreenY: number | null = null;
|
||||||
|
|
||||||
|
const rootEl = $shallowRef<HTMLDivElement>();
|
||||||
|
let scrollEl: HTMLElement | null = null;
|
||||||
|
|
||||||
|
let disabled = false;
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(ev: 'refresh'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function getScrollableParentElement(node) {
|
||||||
|
if (node == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.scrollHeight > node.clientHeight) {
|
||||||
|
return node;
|
||||||
|
} else {
|
||||||
|
return getScrollableParentElement(node.parentNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScreenY(event) {
|
||||||
|
if (supportPointerDesktop) {
|
||||||
|
return event.screenY;
|
||||||
|
}
|
||||||
|
return event.touches[0].screenY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveStart(event) {
|
||||||
|
if (!isPullStart && !isRefreshing && !disabled) {
|
||||||
|
isPullStart = true;
|
||||||
|
startScreenY = getScreenY(event);
|
||||||
|
pullDistance = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveBySystem(to: number): Promise<void> {
|
||||||
|
return new Promise(r => {
|
||||||
|
const startHeight = pullDistance;
|
||||||
|
const overHeight = pullDistance - to;
|
||||||
|
if (overHeight < 1) {
|
||||||
|
r();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const startTime = Date.now();
|
||||||
|
let intervalId = setInterval(() => {
|
||||||
|
const time = Date.now() - startTime;
|
||||||
|
if (time > RELEASE_TRANSITION_DURATION) {
|
||||||
|
pullDistance = to;
|
||||||
|
clearInterval(intervalId);
|
||||||
|
r();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextHeight = startHeight - (overHeight / RELEASE_TRANSITION_DURATION) * time;
|
||||||
|
if (pullDistance < nextHeight) return;
|
||||||
|
pullDistance = nextHeight;
|
||||||
|
}, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fixOverContent() {
|
||||||
|
if (pullDistance > FIRE_THRESHOLD) {
|
||||||
|
await moveBySystem(FIRE_THRESHOLD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeContent() {
|
||||||
|
if (pullDistance > 0) {
|
||||||
|
await moveBySystem(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveEnd() {
|
||||||
|
if (isPullStart && !isRefreshing) {
|
||||||
|
startScreenY = null;
|
||||||
|
if (isPullEnd) {
|
||||||
|
isPullEnd = false;
|
||||||
|
isRefreshing = true;
|
||||||
|
fixOverContent().then(() => emits('refresh'));
|
||||||
|
} else {
|
||||||
|
closeContent().then(() => isPullStart = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moving(event) {
|
||||||
|
if (!isPullStart || isRefreshing || disabled) return;
|
||||||
|
|
||||||
|
if (!scrollEl) {
|
||||||
|
scrollEl = getScrollableParentElement(rootEl);
|
||||||
|
}
|
||||||
|
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) {
|
||||||
|
pullDistance = 0;
|
||||||
|
isPullEnd = false;
|
||||||
|
moveEnd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startScreenY === null) {
|
||||||
|
startScreenY = getScreenY(event);
|
||||||
|
}
|
||||||
|
const moveScreenY = getScreenY(event);
|
||||||
|
|
||||||
|
const moveHeight = moveScreenY - startScreenY!;
|
||||||
|
pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
|
||||||
|
|
||||||
|
isPullEnd = pullDistance >= FIRE_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* emit(refresh)が完了したことを知らせる関数
|
||||||
|
*
|
||||||
|
* タイムアウトがないのでこれを最終的に実行しないと出たままになる
|
||||||
|
*/
|
||||||
|
function refreshFinished() {
|
||||||
|
closeContent().then(() => {
|
||||||
|
isPullStart = false;
|
||||||
|
isRefreshing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDisabled(value) {
|
||||||
|
disabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// マウス操作でpull to refreshするのは不便そう
|
||||||
|
//supportPointerDesktop = !!window.PointerEvent && deviceKind === 'desktop';
|
||||||
|
|
||||||
|
if (supportPointerDesktop) {
|
||||||
|
rootEl.addEventListener('pointerdown', moveStart);
|
||||||
|
// ポインターの場合、ポップアップ系の動作をするとdownだけ発火されてupが発火されないため
|
||||||
|
window.addEventListener('pointerup', moveEnd);
|
||||||
|
rootEl.addEventListener('pointermove', moving, { passive: true });
|
||||||
|
} else {
|
||||||
|
rootEl.addEventListener('touchstart', moveStart);
|
||||||
|
rootEl.addEventListener('touchend', moveEnd);
|
||||||
|
rootEl.addEventListener('touchmove', moving, { passive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (supportPointerDesktop) window.removeEventListener('pointerup', moveEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
refreshFinished,
|
||||||
|
setDisabled,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.frame {
|
||||||
|
position: relative;
|
||||||
|
overflow: clip;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
min-height: var(--frame-min-height, 0px);
|
||||||
|
|
||||||
|
mask-image: linear-gradient(90deg, #000 0%, #000 80%, transparent);
|
||||||
|
-webkit-mask-image: -webkit-linear-gradient(90deg, #000 0%, #000 80%, transparent);
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frameContent {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
margin: 5px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
> .icon, > .loader {
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
transition: transform .25s;
|
||||||
|
|
||||||
|
&.refresh {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slotClip {
|
||||||
|
overflow-y: clip;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -42,7 +42,7 @@ const props = defineProps<{
|
|||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isLong = shouldCollapsed(props.note);
|
const isLong = shouldCollapsed(props.note, []);
|
||||||
|
|
||||||
const collapsed = $ref(isLong);
|
const collapsed = $ref(isLong);
|
||||||
</script>
|
</script>
|
||||||
|
@@ -4,13 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/>
|
<MkPullToRefresh ref="prComponent" @refresh="() => reloadTimeline(true)">
|
||||||
|
<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)" @status="prComponent.setDisabled($event)"/>
|
||||||
|
</MkPullToRefresh>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, provide, onUnmounted } from 'vue';
|
import { computed, provide, onUnmounted } from 'vue';
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotes from '@/components/MkNotes.vue';
|
||||||
import { useStream } from '@/stream.js';
|
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||||
|
import { useStream, reloadStream } from '@/stream.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
@@ -39,6 +42,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
provide('inChannel', computed(() => props.src === 'channel'));
|
provide('inChannel', computed(() => props.src === 'channel'));
|
||||||
|
|
||||||
|
const prComponent: InstanceType<typeof MkPullToRefresh> = $ref();
|
||||||
const tlComponent: InstanceType<typeof MkNotes> = $ref();
|
const tlComponent: InstanceType<typeof MkNotes> = $ref();
|
||||||
|
|
||||||
let tlNotesCount = 0;
|
let tlNotesCount = 0;
|
||||||
@@ -65,29 +69,73 @@ let connection;
|
|||||||
let connection2;
|
let connection2;
|
||||||
|
|
||||||
const stream = useStream();
|
const stream = useStream();
|
||||||
|
const connectChannel = () => {
|
||||||
|
if (props.src === 'antenna') {
|
||||||
|
connection = stream.useChannel('antenna', {
|
||||||
|
antennaId: props.antenna,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'home') {
|
||||||
|
connection = stream.useChannel('homeTimeline', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
});
|
||||||
|
connection2 = stream.useChannel('main');
|
||||||
|
} else if (props.src === 'local') {
|
||||||
|
connection = stream.useChannel('localTimeline', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'social') {
|
||||||
|
connection = stream.useChannel('hybridTimeline', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'global') {
|
||||||
|
connection = stream.useChannel('globalTimeline', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'mentions') {
|
||||||
|
connection = stream.useChannel('main');
|
||||||
|
connection.on('mention', prepend);
|
||||||
|
} else if (props.src === 'directs') {
|
||||||
|
const onNote = note => {
|
||||||
|
if (note.visibility === 'specified') {
|
||||||
|
prepend(note);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
connection = stream.useChannel('main');
|
||||||
|
connection.on('mention', onNote);
|
||||||
|
} else if (props.src === 'list') {
|
||||||
|
connection = stream.useChannel('userList', {
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
listId: props.list,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'channel') {
|
||||||
|
connection = stream.useChannel('channel', {
|
||||||
|
channelId: props.channel,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'role') {
|
||||||
|
connection = stream.useChannel('roleTimeline', {
|
||||||
|
roleId: props.role,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (props.src !== 'directs' || props.src !== 'mentions') connection.on('note', prepend);
|
||||||
|
};
|
||||||
|
|
||||||
if (props.src === 'antenna') {
|
if (props.src === 'antenna') {
|
||||||
endpoint = 'antennas/notes';
|
endpoint = 'antennas/notes';
|
||||||
query = {
|
query = {
|
||||||
antennaId: props.antenna,
|
antennaId: props.antenna,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('antenna', {
|
|
||||||
antennaId: props.antenna,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
} else if (props.src === 'home') {
|
} else if (props.src === 'home') {
|
||||||
endpoint = 'notes/timeline';
|
endpoint = 'notes/timeline';
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('homeTimeline', {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
|
|
||||||
connection2 = stream.useChannel('main');
|
|
||||||
} else if (props.src === 'local') {
|
} else if (props.src === 'local') {
|
||||||
endpoint = 'notes/local-timeline';
|
endpoint = 'notes/local-timeline';
|
||||||
query = {
|
query = {
|
||||||
@@ -95,12 +143,6 @@ if (props.src === 'antenna') {
|
|||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('localTimeline', {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
} else if (props.src === 'social') {
|
} else if (props.src === 'social') {
|
||||||
endpoint = 'notes/hybrid-timeline';
|
endpoint = 'notes/hybrid-timeline';
|
||||||
query = {
|
query = {
|
||||||
@@ -108,68 +150,44 @@ if (props.src === 'antenna') {
|
|||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('hybridTimeline', {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
} else if (props.src === 'global') {
|
} else if (props.src === 'global') {
|
||||||
endpoint = 'notes/global-timeline';
|
endpoint = 'notes/global-timeline';
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('globalTimeline', {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
} else if (props.src === 'mentions') {
|
} else if (props.src === 'mentions') {
|
||||||
endpoint = 'notes/mentions';
|
endpoint = 'notes/mentions';
|
||||||
connection = stream.useChannel('main');
|
|
||||||
connection.on('mention', prepend);
|
|
||||||
} else if (props.src === 'directs') {
|
} else if (props.src === 'directs') {
|
||||||
endpoint = 'notes/mentions';
|
endpoint = 'notes/mentions';
|
||||||
query = {
|
query = {
|
||||||
visibility: 'specified',
|
visibility: 'specified',
|
||||||
};
|
};
|
||||||
const onNote = note => {
|
|
||||||
if (note.visibility === 'specified') {
|
|
||||||
prepend(note);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
connection = stream.useChannel('main');
|
|
||||||
connection.on('mention', onNote);
|
|
||||||
} else if (props.src === 'list') {
|
} else if (props.src === 'list') {
|
||||||
endpoint = 'notes/user-list-timeline';
|
endpoint = 'notes/user-list-timeline';
|
||||||
query = {
|
query = {
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
listId: props.list,
|
listId: props.list,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('userList', {
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
listId: props.list,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
} else if (props.src === 'channel') {
|
} else if (props.src === 'channel') {
|
||||||
endpoint = 'channels/timeline';
|
endpoint = 'channels/timeline';
|
||||||
query = {
|
query = {
|
||||||
channelId: props.channel,
|
channelId: props.channel,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('channel', {
|
|
||||||
channelId: props.channel,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
} else if (props.src === 'role') {
|
} else if (props.src === 'role') {
|
||||||
endpoint = 'roles/notes';
|
endpoint = 'roles/notes';
|
||||||
query = {
|
query = {
|
||||||
roleId: props.role,
|
roleId: props.role,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('roleTimeline', {
|
}
|
||||||
roleId: props.role,
|
|
||||||
|
if (!defaultStore.state.disableStreamingTimeline) {
|
||||||
|
connectChannel();
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
connection.dispose();
|
||||||
|
if (connection2) connection2.dispose();
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
@@ -178,9 +196,19 @@ const pagination = {
|
|||||||
params: query,
|
params: query,
|
||||||
};
|
};
|
||||||
|
|
||||||
onUnmounted(() => {
|
const reloadTimeline = (fromPR = false) => {
|
||||||
connection.dispose();
|
tlNotesCount = 0;
|
||||||
if (connection2) connection2.dispose();
|
|
||||||
|
tlComponent.pagingComponent?.reload().then(() => {
|
||||||
|
reloadStream();
|
||||||
|
if (fromPR) prComponent.refreshFinished();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//const pullRefresh = () => reloadTimeline(true);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
reloadTimeline,
|
||||||
});
|
});
|
||||||
|
|
||||||
/* TODO
|
/* TODO
|
||||||
|
32
packages/frontend/src/components/global/MkFooterSpacer.vue
Normal file
32
packages/frontend/src/components/global/MkFooterSpacer.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="[$style.spacer, defaultStore.reactiveState.darkMode.value ? $style.dark : $style.light]"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.spacer {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 32px;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 300px;
|
||||||
|
background-clip: content-box;
|
||||||
|
background-size: auto auto;
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
|
||||||
|
&.light {
|
||||||
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 16px, #00000010 16px, #00000010 20px );
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 16px, #FFFFFF16 16px, #FFFFFF16 20px );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -38,6 +38,7 @@ type MfmProps = {
|
|||||||
emojiUrls?: string[];
|
emojiUrls?: string[];
|
||||||
rootScale?: number;
|
rootScale?: number;
|
||||||
nyaize: boolean | 'account';
|
nyaize: boolean | 'account';
|
||||||
|
parsedNodes?: mfm.MfmNode[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@@ -48,7 +49,7 @@ export default function(props: MfmProps) {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (props.text == null || props.text === '') return;
|
if (props.text == null || props.text === '') return;
|
||||||
|
|
||||||
const rootAst = (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
|
const rootAst = props.parsedNodes ?? (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
|
||||||
|
|
||||||
const validTime = (t: string | null | undefined) => {
|
const validTime = (t: string | null | undefined) => {
|
||||||
if (t == null) return null;
|
if (t == null) return null;
|
||||||
|
@@ -134,9 +134,11 @@ async function enter(el: HTMLElement) {
|
|||||||
|
|
||||||
setTimeout(renderTab, 170);
|
setTimeout(renderTab, 170);
|
||||||
}
|
}
|
||||||
|
|
||||||
function afterEnter(el: HTMLElement) {
|
function afterEnter(el: HTMLElement) {
|
||||||
//el.style.width = '';
|
//el.style.width = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function leave(el: HTMLElement) {
|
async function leave(el: HTMLElement) {
|
||||||
const elementWidth = el.getBoundingClientRect().width;
|
const elementWidth = el.getBoundingClientRect().width;
|
||||||
el.style.width = elementWidth + 'px';
|
el.style.width = elementWidth + 'px';
|
||||||
@@ -145,6 +147,7 @@ async function leave(el: HTMLElement) {
|
|||||||
el.style.width = '0';
|
el.style.width = '0';
|
||||||
el.style.paddingLeft = '0';
|
el.style.paddingLeft = '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
function afterLeave(el: HTMLElement) {
|
function afterLeave(el: HTMLElement) {
|
||||||
el.style.width = '';
|
el.style.width = '';
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { App } from 'vue';
|
import { App } from 'vue';
|
||||||
|
|
||||||
import Mfm from './global/MkMisskeyFlavoredMarkdown.ts';
|
import Mfm from './global/MkMisskeyFlavoredMarkdown.js';
|
||||||
import MkA from './global/MkA.vue';
|
import MkA from './global/MkA.vue';
|
||||||
import MkAcct from './global/MkAcct.vue';
|
import MkAcct from './global/MkAcct.vue';
|
||||||
import MkAvatar from './global/MkAvatar.vue';
|
import MkAvatar from './global/MkAvatar.vue';
|
||||||
@@ -16,13 +16,14 @@ import MkUserName from './global/MkUserName.vue';
|
|||||||
import MkEllipsis from './global/MkEllipsis.vue';
|
import MkEllipsis from './global/MkEllipsis.vue';
|
||||||
import MkTime from './global/MkTime.vue';
|
import MkTime from './global/MkTime.vue';
|
||||||
import MkUrl from './global/MkUrl.vue';
|
import MkUrl from './global/MkUrl.vue';
|
||||||
import I18n from './global/i18n';
|
import I18n from './global/i18n.js';
|
||||||
import RouterView from './global/RouterView.vue';
|
import RouterView from './global/RouterView.vue';
|
||||||
import MkLoading from './global/MkLoading.vue';
|
import MkLoading from './global/MkLoading.vue';
|
||||||
import MkError from './global/MkError.vue';
|
import MkError from './global/MkError.vue';
|
||||||
import MkAd from './global/MkAd.vue';
|
import MkAd from './global/MkAd.vue';
|
||||||
import MkPageHeader from './global/MkPageHeader.vue';
|
import MkPageHeader from './global/MkPageHeader.vue';
|
||||||
import MkSpacer from './global/MkSpacer.vue';
|
import MkSpacer from './global/MkSpacer.vue';
|
||||||
|
import MkFooterSpacer from './global/MkFooterSpacer.vue';
|
||||||
import MkStickyContainer from './global/MkStickyContainer.vue';
|
import MkStickyContainer from './global/MkStickyContainer.vue';
|
||||||
|
|
||||||
export default function(app: App) {
|
export default function(app: App) {
|
||||||
@@ -50,6 +51,7 @@ export const components = {
|
|||||||
MkAd: MkAd,
|
MkAd: MkAd,
|
||||||
MkPageHeader: MkPageHeader,
|
MkPageHeader: MkPageHeader,
|
||||||
MkSpacer: MkSpacer,
|
MkSpacer: MkSpacer,
|
||||||
|
MkFooterSpacer: MkFooterSpacer,
|
||||||
MkStickyContainer: MkStickyContainer,
|
MkStickyContainer: MkStickyContainer,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,6 +75,7 @@ declare module '@vue/runtime-core' {
|
|||||||
MkAd: typeof MkAd;
|
MkAd: typeof MkAd;
|
||||||
MkPageHeader: typeof MkPageHeader;
|
MkPageHeader: typeof MkPageHeader;
|
||||||
MkSpacer: typeof MkSpacer;
|
MkSpacer: typeof MkSpacer;
|
||||||
|
MkFooterSpacer: typeof MkFooterSpacer;
|
||||||
MkStickyContainer: typeof MkStickyContainer;
|
MkStickyContainer: typeof MkStickyContainer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -66,6 +66,7 @@ export const ROLE_POLICIES = [
|
|||||||
'inviteLimitCycle',
|
'inviteLimitCycle',
|
||||||
'inviteExpirationTime',
|
'inviteExpirationTime',
|
||||||
'canManageCustomEmojis',
|
'canManageCustomEmojis',
|
||||||
|
'canManageAvatarDecorations',
|
||||||
'canSearchNotes',
|
'canSearchNotes',
|
||||||
'canUseTranslator',
|
'canUseTranslator',
|
||||||
'canHideAds',
|
'canHideAds',
|
||||||
|
@@ -1061,7 +1061,7 @@
|
|||||||
["💰", "moneybag", 6],
|
["💰", "moneybag", 6],
|
||||||
["🪙", "coin", 6],
|
["🪙", "coin", 6],
|
||||||
["💳", "credit_card", 6],
|
["💳", "credit_card", 6],
|
||||||
["🪫", "identification_card", 6],
|
["🪪", "identification_card", 6],
|
||||||
["💎", "gem", 6],
|
["💎", "gem", 6],
|
||||||
["⚖", "balance_scale", 6],
|
["⚖", "balance_scale", 6],
|
||||||
["🧰", "toolbox", 6],
|
["🧰", "toolbox", 6],
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
import { computed, reactive } from 'vue';
|
import { computed, reactive } from 'vue';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { openInstanceMenu } from '@/ui/_common_/common.js';
|
import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js';
|
||||||
import { lookup } from '@/scripts/lookup.js';
|
import { lookup } from '@/scripts/lookup.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
@@ -19,6 +19,15 @@ export const navbarItemDef = reactive({
|
|||||||
icon: 'ti ti-bell',
|
icon: 'ti ti-bell',
|
||||||
show: computed(() => $i != null),
|
show: computed(() => $i != null),
|
||||||
indicated: computed(() => $i != null && $i.hasUnreadNotification),
|
indicated: computed(() => $i != null && $i.hasUnreadNotification),
|
||||||
|
indicateValue: computed(() => {
|
||||||
|
if (!$i || $i.unreadNotificationsCount === 0) return '';
|
||||||
|
|
||||||
|
if ($i.unreadNotificationsCount > 99) {
|
||||||
|
return '99+';
|
||||||
|
} else {
|
||||||
|
return $i.unreadNotificationsCount.toString();
|
||||||
|
}
|
||||||
|
}),
|
||||||
to: '/my/notifications',
|
to: '/my/notifications',
|
||||||
},
|
},
|
||||||
drive: {
|
drive: {
|
||||||
@@ -142,6 +151,13 @@ export const navbarItemDef = reactive({
|
|||||||
openInstanceMenu(ev);
|
openInstanceMenu(ev);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
tools: {
|
||||||
|
title: i18n.ts.tools,
|
||||||
|
icon: 'ti ti-tool',
|
||||||
|
action: (ev) => {
|
||||||
|
openToolsMenu(ev);
|
||||||
|
},
|
||||||
|
},
|
||||||
reload: {
|
reload: {
|
||||||
title: i18n.ts.reload,
|
title: i18n.ts.reload,
|
||||||
icon: 'ti ti-refresh',
|
icon: 'ti ti-refresh',
|
||||||
|
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
|
<template #label>{{ i18n.ts._aboutMisskey.projectMembers }}</template>
|
||||||
<div :class="$style.contributors">
|
<div :class="$style.contributors">
|
||||||
<a href="https://github.com/syuilo" target="_blank" :class="$style.contributor">
|
<a href="https://github.com/syuilo" target="_blank" :class="$style.contributor">
|
||||||
<img src="https://avatars.githubusercontent.com/u/4439005?v=4" :class="$style.contributorAvatar">
|
<img src="https://avatars.githubusercontent.com/u/4439005?v=4" :class="$style.contributorAvatar">
|
||||||
@@ -61,20 +61,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<img src="https://avatars.githubusercontent.com/u/20679825?v=4" :class="$style.contributorAvatar">
|
<img src="https://avatars.githubusercontent.com/u/20679825?v=4" :class="$style.contributorAvatar">
|
||||||
<span :class="$style.contributorUsername">@acid-chicken</span>
|
<span :class="$style.contributorUsername">@acid-chicken</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/rinsuki" target="_blank" :class="$style.contributor">
|
<a href="https://github.com/kakkokari-gtyih" target="_blank" :class="$style.contributor">
|
||||||
<img src="https://avatars.githubusercontent.com/u/6533808?v=4" :class="$style.contributorAvatar">
|
<img src="https://avatars.githubusercontent.com/u/67428053?v=4" :class="$style.contributorAvatar">
|
||||||
<span :class="$style.contributorUsername">@rinsuki</span>
|
<span :class="$style.contributorUsername">@kakkokari-gtyih</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/mei23" target="_blank" :class="$style.contributor">
|
<a href="https://github.com/taichanNE30" target="_blank" :class="$style.contributor">
|
||||||
<img src="https://avatars.githubusercontent.com/u/30769358?v=4" :class="$style.contributorAvatar">
|
<img src="https://avatars.githubusercontent.com/u/40626578?v=4" :class="$style.contributorAvatar">
|
||||||
<span :class="$style.contributorUsername">@mei23</span>
|
<span :class="$style.contributorUsername">@taichanNE30</span>
|
||||||
</a>
|
|
||||||
<a href="https://github.com/robflop" target="_blank" :class="$style.contributor">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/8159402?v=4" :class="$style.contributorAvatar">
|
|
||||||
<span :class="$style.contributorUsername">@robflop</span>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
|
</FormSection>
|
||||||
|
<FormSection>
|
||||||
|
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
|
||||||
|
<MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
|
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
|
||||||
@@ -95,6 +94,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div>
|
<div>
|
||||||
<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
|
<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<a style="display: inline-block;" class="xserver" title="XServer" href="https://www.xserver.ne.jp/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/xserver.png" alt="XServer"></a>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a>
|
<a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header>
|
<template #header>
|
||||||
<XHeader :actions="headerActions" :tabs="headerTabs" />
|
<XHeader :actions="headerActions" :tabs="headerTabs"/>
|
||||||
</template>
|
</template>
|
||||||
<MkSpacer :contentMax="900">
|
<MkSpacer :contentMax="900">
|
||||||
<MkSwitch :modelValue="publishing" @update:modelValue="onChangePublishing">
|
<MkSwitch :modelValue="publishing" @update:modelValue="onChangePublishing">
|
||||||
@@ -14,11 +14,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<div>
|
<div>
|
||||||
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
|
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
|
||||||
<MkAd v-if="ad.url" :specify="ad" />
|
<MkAd v-if="ad.url" :specify="ad"/>
|
||||||
<MkInput v-model="ad.url" type="url">
|
<MkInput v-model="ad.url" type="url">
|
||||||
<template #label>URL</template>
|
<template #label>URL</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-model="ad.imageUrl">
|
<MkInput v-model="ad.imageUrl" type="url">
|
||||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkRadios v-model="ad.place">
|
<MkRadios v-model="ad.place">
|
||||||
@@ -51,8 +51,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<span>
|
<span>
|
||||||
{{ i18n.ts._ad.timezoneinfo }}
|
{{ i18n.ts._ad.timezoneinfo }}
|
||||||
<div v-for="(day, index) in daysOfWeek" :key="index">
|
<div v-for="(day, index) in daysOfWeek" :key="index">
|
||||||
<input :id="`ad${ad.id}-${index}`" type="checkbox" :checked="(ad.dayOfWeek & (1 << index)) !== 0"
|
<input
|
||||||
@change="toggleDayOfWeek(ad, index)">
|
:id="`ad${ad.id}-${index}`" type="checkbox" :checked="(ad.dayOfWeek & (1 << index)) !== 0"
|
||||||
|
@change="toggleDayOfWeek(ad, index)"
|
||||||
|
>
|
||||||
<label :for="`ad${ad.id}-${index}`">{{ day }}</label>
|
<label :for="`ad${ad.id}-${index}`">{{ day }}</label>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
@@ -61,9 +63,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #label>{{ i18n.ts.memo }}</template>
|
<template #label>{{ i18n.ts.memo }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i
|
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)">
|
||||||
class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
<i
|
||||||
<MkButton class="button" inline danger @click="remove(ad)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}
|
class="ti ti-device-floppy"
|
||||||
|
></i> {{ i18n.ts.save }}
|
||||||
|
</MkButton>
|
||||||
|
<MkButton class="button" inline danger @click="remove(ad)">
|
||||||
|
<i class="ti ti-trash"></i> {{ i18n.ts.remove }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,6 +121,7 @@ const onChangePublishing = (v) => {
|
|||||||
publishing = v;
|
publishing = v;
|
||||||
refresh();
|
refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 選択された曜日(index)のビットフラグを操作する
|
// 選択された曜日(index)のビットフラグを操作する
|
||||||
function toggleDayOfWeek(ad, index) {
|
function toggleDayOfWeek(ad, index) {
|
||||||
ad.dayOfWeek ^= 1 << index;
|
ad.dayOfWeek ^= 1 << index;
|
||||||
@@ -187,6 +194,7 @@ function save(ad) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function more() {
|
function more() {
|
||||||
os.api('admin/ad/list', { untilId: ads.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => {
|
os.api('admin/ad/list', { untilId: ads.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => {
|
||||||
ads = ads.concat(adsResponse.map(r => {
|
ads = ads.concat(adsResponse.map(r => {
|
||||||
|
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkTextarea v-model="announcement.text">
|
<MkTextarea v-model="announcement.text">
|
||||||
<template #label>{{ i18n.ts.text }}</template>
|
<template #label>{{ i18n.ts.text }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
<MkInput v-model="announcement.imageUrl">
|
<MkInput v-model="announcement.imageUrl" type="url">
|
||||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkRadios v-model="announcement.icon">
|
<MkRadios v-model="announcement.icon">
|
||||||
|
@@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||||
<FormSuspense :p="init">
|
<FormSuspense :p="init">
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkInput v-model="iconUrl">
|
<MkInput v-model="iconUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
|
<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="app192IconUrl">
|
<MkInput v-model="app192IconUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
|
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
|
||||||
<template #caption>
|
<template #caption>
|
||||||
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="app512IconUrl">
|
<MkInput v-model="app512IconUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
|
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
|
||||||
<template #caption>
|
<template #caption>
|
||||||
@@ -37,27 +37,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="bannerUrl">
|
<MkInput v-model="bannerUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.bannerUrl }}</template>
|
<template #label>{{ i18n.ts.bannerUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="backgroundImageUrl">
|
<MkInput v-model="backgroundImageUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
|
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="notFoundImageUrl">
|
<MkInput v-model="notFoundImageUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.notFoundDescription }}</template>
|
<template #label>{{ i18n.ts.notFoundDescription }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="infoImageUrl">
|
<MkInput v-model="infoImageUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.nothing }}</template>
|
<template #label>{{ i18n.ts.nothing }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="serverErrorImageUrl">
|
<MkInput v-model="serverErrorImageUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.somethingHappened }}</template>
|
<template #label>{{ i18n.ts.somethingHappened }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
@@ -118,7 +118,7 @@ const menuDef = $computed(() => [{
|
|||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-sparkles',
|
icon: 'ti ti-sparkles',
|
||||||
text: i18n.ts.avatarDecorations,
|
text: i18n.ts.avatarDecorations,
|
||||||
to: '/admin/avatar-decorations',
|
to: '/avatar-decorations',
|
||||||
active: currentPage?.route.name === 'avatarDecorations',
|
active: currentPage?.route.name === 'avatarDecorations',
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-whirl',
|
icon: 'ti ti-whirl',
|
||||||
|
@@ -20,12 +20,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
||||||
|
|
||||||
<MkInput v-model="tosUrl">
|
<MkInput v-model="tosUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.tosUrl }}</template>
|
<template #label>{{ i18n.ts.tosUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="privacyPolicyUrl">
|
<MkInput v-model="privacyPolicyUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
|
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
|
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
|
||||||
|
|
||||||
<template v-if="useObjectStorage">
|
<template v-if="useObjectStorage">
|
||||||
<MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'">
|
<MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'" type="url">
|
||||||
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
|
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
|
||||||
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
|
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #label>{{ i18n.ts.color }}</template>
|
<template #label>{{ i18n.ts.color }}</template>
|
||||||
</MkColorInput>
|
</MkColorInput>
|
||||||
|
|
||||||
<MkInput v-model="role.iconUrl">
|
<MkInput v-model="role.iconUrl" type="url">
|
||||||
<template #label>{{ i18n.ts._role.iconUrl }}</template>
|
<template #label>{{ i18n.ts._role.iconUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
@@ -259,6 +259,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageAvatarDecorations, 'canManageAvatarDecorations'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.canManageAvatarDecorations }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="role.policies.canManageAvatarDecorations.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||||
|
<span v-else>{{ role.policies.canManageAvatarDecorations.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||||
|
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canManageAvatarDecorations)"></i></span>
|
||||||
|
</template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="role.policies.canManageAvatarDecorations.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="role.policies.canManageAvatarDecorations.value" :disabled="role.policies.canManageAvatarDecorations.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkRange v-model="role.policies.canManageAvatarDecorations.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||||
|
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
@@ -79,6 +79,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</MkInput>
|
</MkInput>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageAvatarDecorations, 'canManageAvatarDecorations'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.canManageAvatarDecorations }}</template>
|
||||||
|
<template #suffix>{{ policies.canManageAvatarDecorations ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
<MkSwitch v-model="policies.canManageAvatarDecorations">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||||
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</MkInput>
|
</MkInput>
|
||||||
</FormSplit>
|
</FormSplit>
|
||||||
|
|
||||||
<MkInput v-model="impressumUrl">
|
<MkInput v-model="impressumUrl" type="url">
|
||||||
<template #label>{{ i18n.ts.impressumUrl }}</template>
|
<template #label>{{ i18n.ts.impressumUrl }}</template>
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #caption>{{ i18n.ts.impressumDescription }}</template>
|
<template #caption>{{ i18n.ts.impressumDescription }}</template>
|
||||||
|
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :contentMax="900">
|
<MkSpacer :contentMax="900">
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
|
<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
|
||||||
@@ -35,7 +35,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import XHeader from './_header_.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';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
@@ -154,12 +154,9 @@ function save() {
|
|||||||
|
|
||||||
if (props.channelId) {
|
if (props.channelId) {
|
||||||
params.channelId = props.channelId;
|
params.channelId = props.channelId;
|
||||||
os.api('channels/update', params).then(() => {
|
os.apiWithDialog('channels/update', params);
|
||||||
os.success();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
os.api('channels/create', params).then(created => {
|
os.apiWithDialog('channels/create', params).then(created => {
|
||||||
os.success();
|
|
||||||
router.push(`/channels/${created.id}`);
|
router.push(`/channels/${created.id}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #icon><i class="ti ti-code"></i></template>
|
<template #icon><i class="ti ti-code"></i></template>
|
||||||
<template #label>{{ i18n.ts._play.viewSource }}</template>
|
<template #label>{{ i18n.ts._play.viewSource }}</template>
|
||||||
|
|
||||||
<MkCode :code="flash.script" :inline="false" class="_monospace"/>
|
<MkCode :code="flash.script" lang="is" :inline="false" class="_monospace"/>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<div :class="$style.footer">
|
<div :class="$style.footer">
|
||||||
<Mfm :text="`By @${flash.user.username}`"/>
|
<Mfm :text="`By @${flash.user.username}`"/>
|
||||||
|
@@ -36,8 +36,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
|
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
|
||||||
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
||||||
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
||||||
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
|
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
@@ -171,8 +171,8 @@ async function fetch(): Promise<void> {
|
|||||||
});
|
});
|
||||||
suspended = instance.isSuspended;
|
suspended = instance.isSuspended;
|
||||||
isBlocked = instance.isBlocked;
|
isBlocked = instance.isBlocked;
|
||||||
isSilenced = instance.isSilenced;
|
isSilenced = instance.isSilenced;
|
||||||
faviconUrl = getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.iconUrl, 'preview');
|
faviconUrl = getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.iconUrl, 'preview');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleBlock(): Promise<void> {
|
async function toggleBlock(): Promise<void> {
|
||||||
@@ -183,14 +183,16 @@ async function toggleBlock(): Promise<void> {
|
|||||||
blockedHosts: isBlocked ? meta.blockedHosts.concat([host]) : meta.blockedHosts.filter(x => x !== host),
|
blockedHosts: isBlocked ? meta.blockedHosts.concat([host]) : meta.blockedHosts.filter(x => x !== host),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleSilenced(): Promise<void> {
|
async function toggleSilenced(): Promise<void> {
|
||||||
if (!meta) throw new Error('No meta?');
|
if (!meta) throw new Error('No meta?');
|
||||||
if (!instance) throw new Error('No instance?');
|
if (!instance) throw new Error('No instance?');
|
||||||
const { host } = instance;
|
const { host } = instance;
|
||||||
await os.api('admin/update-meta', {
|
await os.api('admin/update-meta', {
|
||||||
silencedHosts: isSilenced ? meta.silencedHosts.concat([host]) : meta.silencedHosts.filter(x => x !== host),
|
silencedHosts: isSilenced ? meta.silencedHosts.concat([host]) : meta.silencedHosts.filter(x => x !== host),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleSuspend(): Promise<void> {
|
async function toggleSuspend(): Promise<void> {
|
||||||
if (!instance) throw new Error('No instance?');
|
if (!instance) throw new Error('No instance?');
|
||||||
await os.api('admin/federation/update-instance', {
|
await os.api('admin/federation/update-instance', {
|
||||||
|
@@ -4,46 +4,46 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkSpacer :contentMax="800">
|
<MkStickyContainer>
|
||||||
<div :class="$style.root">
|
<template #header><MkPageHeader/></template>
|
||||||
<div :class="$style.editor" class="_panel">
|
|
||||||
<PrismEditor v-model="code" class="_monospace" :class="$style.code" :highlight="highlighter" :lineNumbers="false"/>
|
|
||||||
<MkButton style="position: absolute; top: 8px; right: 8px;" primary @click="run()"><i class="ti ti-player-play"></i></MkButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MkContainer v-if="root && components.length > 1" :key="uiKey" :foldable="true">
|
<MkSpacer :contentMax="800">
|
||||||
<template #header>UI</template>
|
<div :class="$style.root">
|
||||||
<div :class="$style.ui">
|
<div class="_gaps_s">
|
||||||
<MkAsUi :component="root" :components="components" size="small"/>
|
<div :class="$style.editor" class="_panel">
|
||||||
|
<MkCodeEditor v-model="code" lang="aiscript"/>
|
||||||
|
</div>
|
||||||
|
<MkButton primary @click="run()"><i class="ti ti-player-play"></i></MkButton>
|
||||||
</div>
|
</div>
|
||||||
</MkContainer>
|
|
||||||
|
|
||||||
<MkContainer :foldable="true" class="">
|
<MkContainer v-if="root && components.length > 1" :key="uiKey" :foldable="true">
|
||||||
<template #header>{{ i18n.ts.output }}</template>
|
<template #header>UI</template>
|
||||||
<div :class="$style.logs">
|
<div :class="$style.ui">
|
||||||
<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
|
<MkAsUi :component="root" :components="components" size="small"/>
|
||||||
|
</div>
|
||||||
|
</MkContainer>
|
||||||
|
|
||||||
|
<MkContainer :foldable="true" class="">
|
||||||
|
<template #header>{{ i18n.ts.output }}</template>
|
||||||
|
<div :class="$style.logs">
|
||||||
|
<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
|
||||||
|
</div>
|
||||||
|
</MkContainer>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
{{ i18n.ts.scratchpadDescription }}
|
||||||
</div>
|
</div>
|
||||||
</MkContainer>
|
|
||||||
|
|
||||||
<div class="">
|
|
||||||
{{ i18n.ts.scratchpadDescription }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</MkSpacer>
|
||||||
</MkSpacer>
|
</MkStickyContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onDeactivated, onUnmounted, Ref, ref, watch } from 'vue';
|
import { onDeactivated, onUnmounted, Ref, ref, watch } from 'vue';
|
||||||
import 'prismjs';
|
|
||||||
import { highlight, languages } from 'prismjs/components/prism-core';
|
|
||||||
import 'prismjs/components/prism-clike';
|
|
||||||
import 'prismjs/components/prism-javascript';
|
|
||||||
import 'prismjs/themes/prism-okaidia.css';
|
|
||||||
import { PrismEditor } from 'vue-prism-editor';
|
|
||||||
import 'vue-prism-editor/dist/prismeditor.min.css';
|
|
||||||
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||||
import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
|
import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
@@ -152,10 +152,6 @@ async function run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlighter(code) {
|
|
||||||
return highlight(code, languages.js, 'javascript');
|
|
||||||
}
|
|
||||||
|
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
if (aiscript) aiscript.abort();
|
if (aiscript) aiscript.abort();
|
||||||
});
|
});
|
||||||
|
@@ -88,6 +88,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #label>{{ i18n.ts.notificationDisplay }}</template>
|
<template #label>{{ i18n.ts.notificationDisplay }}</template>
|
||||||
|
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
|
<MkSwitch v-model="useGroupedNotifications">{{ i18n.ts.useGroupedNotifications }}</MkSwitch>
|
||||||
|
|
||||||
<MkRadios v-model="notificationPosition">
|
<MkRadios v-model="notificationPosition">
|
||||||
<template #label>{{ i18n.ts.position }}</template>
|
<template #label>{{ i18n.ts.position }}</template>
|
||||||
<option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
|
<option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
|
||||||
@@ -151,6 +153,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch>
|
<MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch>
|
||||||
<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
|
<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
|
||||||
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
|
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
<MkSelect v-model="serverDisconnectedBehavior">
|
<MkSelect v-model="serverDisconnectedBehavior">
|
||||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||||
@@ -253,6 +256,8 @@ const notificationPosition = computed(defaultStore.makeGetterSetter('notificatio
|
|||||||
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
|
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
|
||||||
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
|
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
|
||||||
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
|
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
|
||||||
|
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
|
||||||
|
const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
|
||||||
|
|
||||||
watch(lang, () => {
|
watch(lang, () => {
|
||||||
miLocalStorage.setItem('lang', lang.value as string);
|
miLocalStorage.setItem('lang', lang.value as string);
|
||||||
@@ -289,6 +294,7 @@ watch([
|
|||||||
reactionsDisplaySize,
|
reactionsDisplaySize,
|
||||||
highlightSensitiveMedia,
|
highlightSensitiveMedia,
|
||||||
keepScreenOn,
|
keepScreenOn,
|
||||||
|
disableStreamingTimeline,
|
||||||
], async () => {
|
], async () => {
|
||||||
await reloadAsk();
|
await reloadAsk();
|
||||||
});
|
});
|
||||||
@@ -298,12 +304,14 @@ const emojiIndexLangs = ['en-US'];
|
|||||||
function downloadEmojiIndex(lang: string) {
|
function downloadEmojiIndex(lang: string) {
|
||||||
async function main() {
|
async function main() {
|
||||||
const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
|
const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
|
||||||
|
|
||||||
function download() {
|
function download() {
|
||||||
switch (lang) {
|
switch (lang) {
|
||||||
case 'en-US': return import('../../unicode-emoji-indexes/en-US.json').then(x => x.default);
|
case 'en-US': return import('../../unicode-emoji-indexes/en-US.json').then(x => x.default);
|
||||||
default: throw new Error('unrecognized lang: ' + lang);
|
default: throw new Error('unrecognized lang: ' + lang);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentIndexes[lang] = await download();
|
currentIndexes[lang] = await download();
|
||||||
await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
|
await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
|
||||||
}
|
}
|
||||||
@@ -340,6 +348,7 @@ function removePinnedList() {
|
|||||||
|
|
||||||
let smashCount = 0;
|
let smashCount = 0;
|
||||||
let smashTimer: number | null = null;
|
let smashTimer: number | null = null;
|
||||||
|
|
||||||
function testNotification(): void {
|
function testNotification(): void {
|
||||||
const notification: Misskey.entities.Notification = {
|
const notification: Misskey.entities.Notification = {
|
||||||
id: Math.random().toString(),
|
id: Math.random().toString(),
|
||||||
|
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
|
<MkFooterSpacer/>
|
||||||
</mkstickycontainer>
|
</mkstickycontainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@@ -74,11 +74,11 @@ let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage
|
|||||||
const userLists = await os.api('users/lists/list');
|
const userLists = await os.api('users/lists/list');
|
||||||
|
|
||||||
async function readAllUnreadNotes() {
|
async function readAllUnreadNotes() {
|
||||||
await os.api('i/read-all-unread-notes');
|
await os.apiWithDialog('i/read-all-unread-notes');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readAllNotifications() {
|
async function readAllNotifications() {
|
||||||
await os.api('notifications/mark-all-as-read');
|
await os.apiWithDialog('notifications/mark-all-as-read');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateReceiveConfig(type, value) {
|
async function updateReceiveConfig(type, value) {
|
||||||
|
@@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkButton inline @click="copy(plugin)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
|
<MkButton inline @click="copy(plugin)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkCode :code="plugin.src ?? ''"/>
|
<MkCode :code="plugin.src ?? ''" lang="is"/>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user