mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-31 04:15:51 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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 -->
|
||||
@@ -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
|
||||
@@ -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 -->
|
||||
@@ -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
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
- run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v3.8.1
|
||||
- uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v3.8.1
|
||||
- uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v3.8.1
|
||||
- uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
version: 7
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- run: corepack enable
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
+19
-2
@@ -16,7 +16,12 @@
|
||||
|
||||
### General
|
||||
- Feat: アイコンデコレーション機能
|
||||
- サーバーで用意された画像をアイコンに重ねることができます
|
||||
- 画像のテンプレートはこちらです: https://misskey-hub.net/avatar-decoration-template.png
|
||||
- 最大でも黄色いエリア内にデコレーションを収めることを推奨します。
|
||||
- 画像は512x512pxを推奨します。
|
||||
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
|
||||
- Enhance: 未読の通知数を表示できるように
|
||||
- Enhance: ローカリゼーションの更新
|
||||
- Enhance: 依存関係の更新
|
||||
|
||||
@@ -24,6 +29,9 @@
|
||||
- Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました
|
||||
- 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください
|
||||
https://misskey-hub.net/docs/advanced/publish-on-your-website.html
|
||||
- Enhance: スワイプしてタイムラインを再読込できるように
|
||||
- PCの場合は右上のボタンからでも再読込できます
|
||||
- Enhance: タイムラインの自動更新を無効にできるように
|
||||
- Enhance: コードのシンタックスハイライトエンジンをShikiに変更
|
||||
- AiScriptのシンタックスハイライトに対応
|
||||
- MFMでAiScriptをハイライトする場合、コードブロックの開始部分を ` ```is ` もしくは ` ```aiscript ` としてください
|
||||
@@ -31,24 +39,33 @@
|
||||
- Enhance: プラグインを削除した際には、使用されていたアクセストークンも同時に削除されるようになりました
|
||||
- Enhance: プラグインで`Plugin:register_note_view_interruptor`を用いてnoteの代わりにnullを返却することでノートを非表示にできるようになりました
|
||||
- Enhance: AiScript関数`Mk:nyaize()`が追加されました
|
||||
- Enhance: 情報→ツール はナビゲーションバーにツールとして独立した項目になりました
|
||||
- Enhance: その他細かなブラッシュアップ
|
||||
- Fix: 投稿フォームでのユーザー変更がプレビューに反映されない問題を修正
|
||||
- Fix: ユーザーページの ノート > ファイル付き タブにリプライが表示されてしまう
|
||||
- Fix: 「検索」MFMにおいて一部の検索キーワードが正しく認識されない問題を修正
|
||||
- Fix: 一部の言語でMisskey Webがクラッシュする問題を修正
|
||||
- Fix: チャンネルの作成・更新時に失敗した場合何も表示されない問題を修正 #11983
|
||||
- Fix: 個人カードのemojiがバッテリーになっている問題を修正
|
||||
- Fix: 標準テーマと同じIDを使用してインストールできてしまう問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: RedisへのTLのキャッシュをオフにできるように
|
||||
- Enhance: RedisへのTLのキャッシュ(FTT)をオフにできるように
|
||||
- Enhance: フォローしているチャンネルをフォロー解除した時(またはその逆)、タイムラインに反映される間隔を改善
|
||||
- Enhance: プロフィールの自己紹介欄のMFMが連合するようになりました
|
||||
- 相手がMisskey v2023.11.0以降である必要があります
|
||||
- Enhance: チャンネル取得時のパフォーマンスを向上
|
||||
- Fix: リストTLに自分のフォロワー限定投稿が含まれない問題を修正
|
||||
- Fix: ローカルタイムラインに投稿者自身の投稿への返信が含まれない問題を修正
|
||||
- Fix: 自分のフォローしているユーザーの自分のフォローしていないユーザーの visibility: followers な投稿への返信がストリーミングで流れてくる問題を修正
|
||||
- Fix: RedisへのTLキャッシュが有効の場合にHTL/LTL/STLが空になることがある問題を修正
|
||||
- Fix: STLでフォローしていないチャンネルが取得される問題を修正
|
||||
- Fix: `hashtags/trend`にてRedisからトレンドの情報が取得できない際にInternal Server Errorになる問題を修正
|
||||
- Fix: HTLをリロードまたは遡行したとき、フォローしているチャンネルのノートが含まれない問題を修正 #11765
|
||||
- Fix: HTLをリロードまたは遡行したとき、フォローしているチャンネルのノートが含まれない問題を修正 #11765 #12181
|
||||
- Fix: リノートをリノートできるのを修正
|
||||
- Fix: アクセストークンを削除すると、通知が取得できなくなる場合がある問題を修正
|
||||
- Fix: 自身の宛先なしダイレクト投稿がストリーミングで流れてこない問題を修正
|
||||
- Fix: サーバーサイドからのテスト通知を正しく行えるように修正
|
||||
|
||||
## 2023.10.2
|
||||
|
||||
|
||||
Vendored
+6
@@ -982,6 +982,7 @@ export interface Locale {
|
||||
"unassign": string;
|
||||
"color": string;
|
||||
"manageCustomEmojis": string;
|
||||
"manageAvatarDecorations": string;
|
||||
"youCannotCreateAnymore": string;
|
||||
"cannotPerformTemporary": string;
|
||||
"cannotPerformTemporaryDescription": string;
|
||||
@@ -1152,6 +1153,10 @@ export interface Locale {
|
||||
"angle": string;
|
||||
"flip": string;
|
||||
"showAvatarDecorations": string;
|
||||
"releaseToRefresh": string;
|
||||
"refreshing": string;
|
||||
"pullDownToRefresh": string;
|
||||
"disableStreamingTimeline": string;
|
||||
"_announcement": {
|
||||
"forExistingUsers": string;
|
||||
"forExistingUsersDescription": string;
|
||||
@@ -1571,6 +1576,7 @@ export interface Locale {
|
||||
"inviteLimitCycle": string;
|
||||
"inviteExpirationTime": string;
|
||||
"canManageCustomEmojis": string;
|
||||
"canManageAvatarDecorations": string;
|
||||
"driveCapacity": string;
|
||||
"alwaysMarkNsfw": string;
|
||||
"pinMax": string;
|
||||
|
||||
@@ -979,6 +979,7 @@ assign: "アサイン"
|
||||
unassign: "アサインを解除"
|
||||
color: "色"
|
||||
manageCustomEmojis: "カスタム絵文字の管理"
|
||||
manageAvatarDecorations: "アバターデコレーションの管理"
|
||||
youCannotCreateAnymore: "これ以上作成することはできません。"
|
||||
cannotPerformTemporary: "一時的に利用できません"
|
||||
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
|
||||
@@ -1149,6 +1150,10 @@ detach: "外す"
|
||||
angle: "角度"
|
||||
flip: "反転"
|
||||
showAvatarDecorations: "アイコンのデコレーションを表示"
|
||||
releaseToRefresh: "離してリロード"
|
||||
refreshing: "リロード中"
|
||||
pullDownToRefresh: "引っ張ってリロード"
|
||||
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
|
||||
|
||||
_announcement:
|
||||
forExistingUsers: "既存ユーザーのみ"
|
||||
@@ -1492,6 +1497,7 @@ _role:
|
||||
inviteLimitCycle: "招待コードの発行間隔"
|
||||
inviteExpirationTime: "招待コードの有効期限"
|
||||
canManageCustomEmojis: "カスタム絵文字の管理"
|
||||
canManageAvatarDecorations: "アバターデコレーションの管理"
|
||||
driveCapacity: "ドライブ容量"
|
||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||
pinMax: "ノートのピン留めの最大数"
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2023.11.0-beta.5",
|
||||
"version": "2023.11.0-beta.7",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -48,14 +48,14 @@
|
||||
"cssnano": "6.0.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"postcss": "8.4.31",
|
||||
"terser": "5.22.0",
|
||||
"terser": "5.24.0",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "6.9.0",
|
||||
"@typescript-eslint/parser": "6.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.9.1",
|
||||
"@typescript-eslint/parser": "6.9.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.3.3",
|
||||
"cypress": "13.4.0",
|
||||
"eslint": "8.52.0",
|
||||
"start-server-and-test": "2.0.1"
|
||||
},
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -66,17 +66,17 @@
|
||||
"@discordapp/twemoji": "14.1.2",
|
||||
"@fastify/accepts": "4.2.0",
|
||||
"@fastify/cookie": "9.1.0",
|
||||
"@fastify/cors": "8.4.0",
|
||||
"@fastify/cors": "8.4.1",
|
||||
"@fastify/express": "2.3.0",
|
||||
"@fastify/http-proxy": "9.2.1",
|
||||
"@fastify/multipart": "8.0.0",
|
||||
"@fastify/static": "6.11.2",
|
||||
"@fastify/static": "6.12.0",
|
||||
"@fastify/view": "8.2.0",
|
||||
"@nestjs/common": "10.2.7",
|
||||
"@nestjs/core": "10.2.7",
|
||||
"@nestjs/testing": "10.2.7",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@simplewebauthn/server": "8.3.4",
|
||||
"@simplewebauthn/server": "8.3.5",
|
||||
"@sinonjs/fake-timers": "11.2.2",
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.95",
|
||||
@@ -87,7 +87,7 @@
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.2",
|
||||
"bullmq": "4.12.6",
|
||||
"bullmq": "4.12.7",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.1",
|
||||
"chalk": "5.3.0",
|
||||
@@ -138,7 +138,7 @@
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
"punycode": "2.3.0",
|
||||
"punycode": "2.3.1",
|
||||
"pureimage": "0.3.17",
|
||||
"qrcode": "1.5.3",
|
||||
"random-seed": "0.3.0",
|
||||
@@ -175,7 +175,7 @@
|
||||
"@simplewebauthn/typescript-types": "8.3.4",
|
||||
"@swc/jest": "0.2.29",
|
||||
"@types/accepts": "1.3.6",
|
||||
"@types/archiver": "5.3.4",
|
||||
"@types/archiver": "6.0.0",
|
||||
"@types/bcryptjs": "2.4.5",
|
||||
"@types/body-parser": "1.19.4",
|
||||
"@types/cbor": "6.0.0",
|
||||
@@ -183,14 +183,14 @@
|
||||
"@types/content-disposition": "0.5.7",
|
||||
"@types/fluent-ffmpeg": "2.1.23",
|
||||
"@types/http-link-header": "1.0.4",
|
||||
"@types/jest": "29.5.6",
|
||||
"@types/jest": "29.5.7",
|
||||
"@types/js-yaml": "4.0.8",
|
||||
"@types/jsdom": "21.1.4",
|
||||
"@types/jsonld": "1.5.11",
|
||||
"@types/jsrsasign": "10.5.11",
|
||||
"@types/mime-types": "2.1.3",
|
||||
"@types/ms": "0.7.33",
|
||||
"@types/node": "20.8.9",
|
||||
"@types/node": "20.8.10",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.13",
|
||||
"@types/oauth": "0.9.3",
|
||||
@@ -213,8 +213,8 @@
|
||||
"@types/vary": "1.1.2",
|
||||
"@types/web-push": "3.6.2",
|
||||
"@types/ws": "8.5.8",
|
||||
"@typescript-eslint/eslint-plugin": "6.9.0",
|
||||
"@typescript-eslint/parser": "6.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.9.1",
|
||||
"@typescript-eslint/parser": "6.9.1",
|
||||
"aws-sdk-client-mock": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.52.0",
|
||||
|
||||
@@ -258,7 +258,7 @@ export function loadConfig(): Config {
|
||||
clientEntry: clientManifest['src/_boot_.ts'],
|
||||
clientManifestExists: clientManifestExists,
|
||||
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
||||
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 300,
|
||||
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
|
||||
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
||||
pidFile: config.pidFile,
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { SearchService } from '@/core/SearchService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteDeleteService {
|
||||
@@ -77,8 +78,8 @@ export class NoteDeleteService {
|
||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
||||
let renote: MiNote | null = null;
|
||||
|
||||
// if deletd note is renote
|
||||
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
|
||||
// if deleted note is renote
|
||||
if (isPureRenote(note)) {
|
||||
renote = await this.notesRepository.findOneBy({
|
||||
id: note.renoteId,
|
||||
});
|
||||
|
||||
@@ -144,7 +144,9 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
||||
|
||||
// 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}`);
|
||||
if (latestReadNotificationId && (latestReadNotificationId >= (await redisIdPromise)!)) return;
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export type RolePolicies = {
|
||||
inviteLimitCycle: number;
|
||||
inviteExpirationTime: number;
|
||||
canManageCustomEmojis: boolean;
|
||||
canManageAvatarDecorations: boolean;
|
||||
canSearchNotes: boolean;
|
||||
canUseTranslator: boolean;
|
||||
canHideAds: boolean;
|
||||
@@ -57,6 +58,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
inviteLimitCycle: 60 * 24 * 7,
|
||||
inviteExpirationTime: 0,
|
||||
canManageCustomEmojis: false,
|
||||
canManageAvatarDecorations: false,
|
||||
canSearchNotes: false,
|
||||
canUseTranslator: true,
|
||||
canHideAds: false,
|
||||
@@ -306,6 +308,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
|
||||
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
||||
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)),
|
||||
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||
|
||||
@@ -319,9 +319,17 @@ export class ApPersonService implements OnModuleInit {
|
||||
emojis,
|
||||
})) 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({
|
||||
userId: user.id,
|
||||
description: person._misskey_summary ? truncate(person._misskey_summary, summaryLength) : person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
||||
description: _description,
|
||||
url,
|
||||
fields,
|
||||
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 }, {
|
||||
url,
|
||||
fields,
|
||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
||||
description: _description,
|
||||
birthday: bday?.[0] ?? null,
|
||||
location: person['vcard:Address'] ?? null,
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
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 { } from '@/models/Blocking.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
@@ -31,9 +31,6 @@ export class ChannelEntityService {
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.noteUnreadsRepository)
|
||||
private noteUnreadsRepository: NoteUnreadsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
@@ -54,13 +51,6 @@ export class ChannelEntityService {
|
||||
|
||||
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({
|
||||
where: {
|
||||
followerId: meId,
|
||||
@@ -99,7 +89,7 @@ export class ChannelEntityService {
|
||||
...(me ? {
|
||||
isFollowing,
|
||||
isFavorited,
|
||||
hasUnreadNote,
|
||||
hasUnreadNote: false, // 後方互換性のため
|
||||
} : {}),
|
||||
|
||||
...(detailed ? {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { In } from 'typeorm';
|
||||
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 type { MiNotification } from '@/models/Notification.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
@@ -40,9 +40,6 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
@Inject(DI.accessTokensRepository)
|
||||
private accessTokensRepository: AccessTokensRepository,
|
||||
|
||||
//private userEntityService: UserEntityService,
|
||||
//private noteEntityService: NoteEntityService,
|
||||
//private customEmojiService: CustomEmojiService,
|
||||
@@ -69,7 +66,6 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
},
|
||||
): Promise<Packed<'Notification'>> {
|
||||
const notification = src;
|
||||
const token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
|
||||
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? (
|
||||
hint?.packedNotes != null
|
||||
? hint.packedNotes.get(notification.noteId)
|
||||
@@ -100,8 +96,8 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
} : {}),
|
||||
...(notification.type === 'app' ? {
|
||||
body: notification.customBody,
|
||||
header: notification.customHeader ?? token?.name,
|
||||
icon: notification.customIcon ?? token?.iconUrl,
|
||||
header: notification.customHeader,
|
||||
icon: notification.customIcon,
|
||||
} : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } 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 { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
@@ -235,17 +236,34 @@ export class UserEntityService implements OnModuleInit {
|
||||
}
|
||||
|
||||
@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 latestNotificationIdsRes = await this.redisClient.xrevrange(
|
||||
`notificationTimeline:${userId}`,
|
||||
'+',
|
||||
'-',
|
||||
'COUNT', 1);
|
||||
const latestNotificationId = latestNotificationIdsRes[0]?.[0];
|
||||
if (!latestReadNotificationId) {
|
||||
response.unreadCount = await this.redisClient.xlen(`notificationTimeline:${userId}`);
|
||||
} else {
|
||||
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
||||
`notificationTimeline:${userId}`,
|
||||
'+',
|
||||
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
|
||||
@@ -331,6 +349,8 @@ export class UserEntityService implements OnModuleInit {
|
||||
...announcement,
|
||||
})) : null;
|
||||
|
||||
const notificationsInfo = isMe && opts.detail ? await this.getNotificationsInfo(user.id) : null;
|
||||
|
||||
const packed = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
@@ -449,8 +469,9 @@ export class UserEntityService implements OnModuleInit {
|
||||
unreadAnnouncements,
|
||||
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
||||
hasUnreadChannel: false, // 後方互換性のため
|
||||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
||||
hasUnreadNotification: notificationsInfo?.hasUnread, // 後方互換性のため
|
||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||
unreadNotificationsCount: notificationsInfo?.unreadCount,
|
||||
mutedWords: profile!.mutedWords,
|
||||
mutedInstances: profile!.mutedInstances,
|
||||
mutingNotificationTypes: [], // 後方互換性のため
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -399,6 +399,10 @@ export const packedMeDetailedOnlySchema = {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
unreadNotificationsCount: {
|
||||
type: 'number',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
mutedWords: {
|
||||
type: 'array',
|
||||
nullable: false, optional: false,
|
||||
|
||||
@@ -26,6 +26,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.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 { FindOptionsWhere } from 'typeorm';
|
||||
|
||||
@@ -88,7 +89,7 @@ export class ActivityPubServerService {
|
||||
*/
|
||||
@bindThis
|
||||
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 });
|
||||
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRolePolicy: 'canManageAvatarDecorations',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
||||
@@ -13,8 +13,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
requireRolePolicy: 'canManageAvatarDecorations',
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -16,7 +16,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRolePolicy: 'canManageAvatarDecorations',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRolePolicy: 'canManageAvatarDecorations',
|
||||
|
||||
errors: {
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
@@ -221,7 +222,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
if (renote == null) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -254,7 +255,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
if (reply == null) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
const followingChannelIds = followingChannels.map(x => x.followeeId);
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds })
|
||||
.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) {
|
||||
|
||||
@@ -42,8 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
this.notificationService.createNotification(user.id, 'app', {
|
||||
appAccessTokenId: token ? token.id : null,
|
||||
customBody: ps.body,
|
||||
customHeader: ps.header,
|
||||
customIcon: ps.icon,
|
||||
customHeader: ps.header ?? token?.name,
|
||||
customIcon: ps.icon ?? token?.iconUrl,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ export default abstract class Channel {
|
||||
}
|
||||
|
||||
public abstract init(params: any): void;
|
||||
|
||||
public dispose?(): void;
|
||||
|
||||
public onMessage?(type: string, body: any): void;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class HomeTimelineChannel extends Channel {
|
||||
if (note.visibility === 'followers') {
|
||||
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
||||
} 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) {
|
||||
|
||||
@@ -67,7 +67,7 @@ class HybridTimelineChannel extends Channel {
|
||||
if (note.visibility === 'followers') {
|
||||
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
||||
} 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
|
||||
|
||||
@@ -720,7 +720,7 @@ describe('クリップ', () => {
|
||||
test('を追加できる。', async () => {
|
||||
await addNote({ clipId: aliceClip.id, noteId: aliceNote.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]);
|
||||
|
||||
// 他人の非公開ノートも突っ込める
|
||||
|
||||
@@ -164,6 +164,7 @@ describe('ユーザー', () => {
|
||||
hasUnreadAntenna: user.hasUnreadAntenna,
|
||||
hasUnreadChannel: user.hasUnreadChannel,
|
||||
hasUnreadNotification: user.hasUnreadNotification,
|
||||
unreadNotificationsCount: user.unreadNotificationsCount,
|
||||
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
|
||||
unreadAnnouncements: user.unreadAnnouncements,
|
||||
mutedWords: user.mutedWords,
|
||||
@@ -414,6 +415,7 @@ describe('ユーザー', () => {
|
||||
assert.strictEqual(response.hasUnreadAntenna, false);
|
||||
assert.strictEqual(response.hasUnreadChannel, false);
|
||||
assert.strictEqual(response.hasUnreadNotification, false);
|
||||
assert.strictEqual(response.unreadNotificationsCount, 0);
|
||||
assert.strictEqual(response.hasPendingReceivedFollowRequest, false);
|
||||
assert.deepStrictEqual(response.unreadAnnouncements, []);
|
||||
assert.deepStrictEqual(response.mutedWords, []);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"build-storybook": "pnpm build-storybook-pre && storybook build",
|
||||
"chromatic": "chromatic",
|
||||
"test": "vitest --run",
|
||||
"test-and-coverage": "vitest --run --coverage",
|
||||
"test-and-coverage": "vitest --run --coverage --globals",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
@@ -20,7 +20,7 @@
|
||||
"@github/webauthn-json": "2.1.1",
|
||||
"@rollup/plugin-alias": "5.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",
|
||||
"@syuilo/aiscript": "0.16.0",
|
||||
"@tabler/icons-webfont": "2.37.0",
|
||||
@@ -30,7 +30,7 @@
|
||||
"astring": "1.8.6",
|
||||
"autosize": "6.0.1",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.5",
|
||||
"broadcast-channel": "5.5.1",
|
||||
"broadcast-channel": "6.0.0",
|
||||
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||
"buraha": "0.0.1",
|
||||
"canvas-confetti": "1.6.1",
|
||||
@@ -39,7 +39,7 @@
|
||||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"chromatic": "7.5.4",
|
||||
"chromatic": "7.6.0",
|
||||
"compare-versions": "6.1.0",
|
||||
"cropperjs": "2.0.0-beta.4",
|
||||
"date-fns": "2.30.0",
|
||||
@@ -55,9 +55,9 @@
|
||||
"mfm-js": "0.23.3",
|
||||
"misskey-js": "workspace:*",
|
||||
"photoswipe": "5.4.2",
|
||||
"punycode": "2.3.0",
|
||||
"punycode": "2.3.1",
|
||||
"querystring": "0.2.1",
|
||||
"rollup": "4.1.4",
|
||||
"rollup": "4.2.0",
|
||||
"sanitize-html": "2.11.0",
|
||||
"shiki": "^0.14.5",
|
||||
"sass": "1.69.5",
|
||||
@@ -78,30 +78,30 @@
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "7.5.1",
|
||||
"@storybook/addon-essentials": "7.5.1",
|
||||
"@storybook/addon-interactions": "7.5.1",
|
||||
"@storybook/addon-links": "7.5.1",
|
||||
"@storybook/addon-storysource": "7.5.1",
|
||||
"@storybook/addons": "7.5.1",
|
||||
"@storybook/blocks": "7.5.1",
|
||||
"@storybook/core-events": "7.5.1",
|
||||
"@storybook/addon-actions": "7.5.2",
|
||||
"@storybook/addon-essentials": "7.5.2",
|
||||
"@storybook/addon-interactions": "7.5.2",
|
||||
"@storybook/addon-links": "7.5.2",
|
||||
"@storybook/addon-storysource": "7.5.2",
|
||||
"@storybook/addons": "7.5.2",
|
||||
"@storybook/blocks": "7.5.2",
|
||||
"@storybook/core-events": "7.5.2",
|
||||
"@storybook/jest": "0.2.3",
|
||||
"@storybook/manager-api": "7.5.1",
|
||||
"@storybook/preview-api": "7.5.1",
|
||||
"@storybook/react": "7.5.1",
|
||||
"@storybook/react-vite": "7.5.1",
|
||||
"@storybook/manager-api": "7.5.2",
|
||||
"@storybook/preview-api": "7.5.2",
|
||||
"@storybook/react": "7.5.2",
|
||||
"@storybook/react-vite": "7.5.2",
|
||||
"@storybook/testing-library": "0.2.2",
|
||||
"@storybook/theming": "7.5.1",
|
||||
"@storybook/types": "7.5.1",
|
||||
"@storybook/vue3": "7.5.1",
|
||||
"@storybook/vue3-vite": "7.5.1",
|
||||
"@testing-library/vue": "7.0.0",
|
||||
"@storybook/theming": "7.5.2",
|
||||
"@storybook/types": "7.5.2",
|
||||
"@storybook/vue3": "7.5.2",
|
||||
"@storybook/vue3-vite": "7.5.2",
|
||||
"@testing-library/vue": "8.0.0",
|
||||
"@types/escape-regexp": "0.0.2",
|
||||
"@types/estree": "1.0.3",
|
||||
"@types/estree": "1.0.4",
|
||||
"@types/matter-js": "0.19.2",
|
||||
"@types/micromatch": "4.0.4",
|
||||
"@types/node": "20.8.9",
|
||||
"@types/node": "20.8.10",
|
||||
"@types/punycode": "2.1.1",
|
||||
"@types/sanitize-html": "2.9.3",
|
||||
"@types/throttle-debounce": "5.0.1",
|
||||
@@ -109,13 +109,13 @@
|
||||
"@types/uuid": "9.0.6",
|
||||
"@types/websocket": "1.0.8",
|
||||
"@types/ws": "8.5.8",
|
||||
"@typescript-eslint/eslint-plugin": "6.9.0",
|
||||
"@typescript-eslint/parser": "6.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.9.1",
|
||||
"@typescript-eslint/parser": "6.9.1",
|
||||
"@vitest/coverage-v8": "0.34.6",
|
||||
"@vue/runtime-core": "3.3.7",
|
||||
"acorn": "8.11.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.3.3",
|
||||
"cypress": "13.4.0",
|
||||
"eslint": "8.52.0",
|
||||
"eslint-plugin-import": "2.29.0",
|
||||
"eslint-plugin-vue": "9.18.1",
|
||||
@@ -129,7 +129,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"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",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { common } from './common.js';
|
||||
import { version, ui, lang, updateLocale } from '@/config.js';
|
||||
import { i18n, updateI18n } from '@/i18n.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 { $i, refreshAccount, login, updateAccount, signout } from '@/account.js';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||
@@ -39,6 +39,7 @@ export async function mainBoot() {
|
||||
|
||||
let reloadDialogShowing = false;
|
||||
stream.on('_disconnected_', async () => {
|
||||
if (isReloading) return;
|
||||
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
|
||||
location.reload();
|
||||
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
|
||||
@@ -225,11 +226,18 @@ export async function mainBoot() {
|
||||
});
|
||||
|
||||
main.on('readAllNotifications', () => {
|
||||
updateAccount({ hasUnreadNotification: false });
|
||||
updateAccount({
|
||||
hasUnreadNotification: false,
|
||||
unreadNotificationsCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
main.on('unreadNotification', () => {
|
||||
updateAccount({ hasUnreadNotification: true });
|
||||
const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
|
||||
updateAccount({
|
||||
hasUnreadNotification: true,
|
||||
unreadNotificationsCount,
|
||||
});
|
||||
});
|
||||
|
||||
main.on('unreadMention', () => {
|
||||
|
||||
@@ -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')">
|
||||
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
|
||||
<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(); }">
|
||||
<i class="icon" :class="item.icon"></i>
|
||||
<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>
|
||||
<MkA v-else v-click-anime :to="item.to" class="item" @click.passive="close()">
|
||||
<i class="icon" :class="item.icon"></i>
|
||||
<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>
|
||||
</template>
|
||||
</div>
|
||||
@@ -27,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import { navbarItemDef } from '@/navbar';
|
||||
import { navbarItemDef } from '@/navbar.js';
|
||||
import { defaultStore } from '@/store.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,
|
||||
action: def.action,
|
||||
indicate: def.indicated,
|
||||
indicateValue: def.indicateValue,
|
||||
}));
|
||||
|
||||
function close() {
|
||||
@@ -116,6 +119,17 @@ function close() {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
> .indicatorWithValue {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: 16px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
top: 16px;
|
||||
left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .indicator {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
|
||||
@@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div :class="$style.text">
|
||||
<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>
|
||||
<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">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
@@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</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>
|
||||
</div>
|
||||
<MkReactionsViewer :note="appearNote" :maxNumber="16">
|
||||
<MkReactionsViewer v-show="appearNote.cw == null || showContent" :note="appearNote" :maxNumber="16">
|
||||
<template #more>
|
||||
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
|
||||
</template>
|
||||
@@ -209,8 +209,9 @@ const clipButton = shallowRef<HTMLElement>();
|
||||
let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
|
||||
const isMyRenote = $i && ($i.id === note.userId);
|
||||
const showContent = ref(false);
|
||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
||||
const isLong = shouldCollapsed(appearNote);
|
||||
const parsed = appearNote.text ? mfm.parse(appearNote.text) : null;
|
||||
const urls = parsed ? extractUrlFromMfm(parsed) : null;
|
||||
const isLong = shouldCollapsed(appearNote, urls ?? []);
|
||||
const collapsed = ref(appearNote.cw == null && isLong);
|
||||
const isDeleted = ref(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">
|
||||
<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>
|
||||
<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>
|
||||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
@@ -260,7 +260,8 @@ const isDeleted = ref(false);
|
||||
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
||||
const translation = ref(null);
|
||||
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 conversation = ref<Misskey.entities.Note[]>([]);
|
||||
const replies = ref<Misskey.entities.Note[]>([]);
|
||||
|
||||
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<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 XNotification from '@/components/MkNotification.vue';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
@@ -68,6 +68,10 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
if (connection) connection.dispose();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
if (connection) connection.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -166,6 +166,8 @@ defineExpose({
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
overscroll-behavior: none;
|
||||
|
||||
min-height: 100%;
|
||||
background: var(--bg);
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ const props = withDefaults(defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'queue', count: number): void;
|
||||
(ev: 'status', error: boolean): void;
|
||||
}>();
|
||||
|
||||
let rootEl = $shallowRef<HTMLElement>();
|
||||
@@ -193,6 +194,11 @@ watch(queue, (a, b) => {
|
||||
emit('queue', queue.value.size);
|
||||
}, { deep: true });
|
||||
|
||||
watch(error, (n, o) => {
|
||||
if (n === o) return;
|
||||
emit('status', n);
|
||||
});
|
||||
|
||||
async function init(): Promise<void> {
|
||||
items.value = new Map();
|
||||
queue.value = new Map();
|
||||
|
||||
@@ -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;
|
||||
}>();
|
||||
|
||||
const isLong = shouldCollapsed(props.note);
|
||||
const isLong = shouldCollapsed(props.note, []);
|
||||
|
||||
const collapsed = $ref(isLong);
|
||||
</script>
|
||||
|
||||
@@ -4,13 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<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>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, onUnmounted } from '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 { $i } from '@/account.js';
|
||||
import { instance } from '@/instance.js';
|
||||
@@ -39,6 +42,7 @@ const emit = defineEmits<{
|
||||
|
||||
provide('inChannel', computed(() => props.src === 'channel'));
|
||||
|
||||
const prComponent: InstanceType<typeof MkPullToRefresh> = $ref();
|
||||
const tlComponent: InstanceType<typeof MkNotes> = $ref();
|
||||
|
||||
let tlNotesCount = 0;
|
||||
@@ -65,29 +69,73 @@ let connection;
|
||||
let connection2;
|
||||
|
||||
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') {
|
||||
endpoint = 'antennas/notes';
|
||||
query = {
|
||||
antennaId: props.antenna,
|
||||
};
|
||||
connection = stream.useChannel('antenna', {
|
||||
antennaId: props.antenna,
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
} else if (props.src === 'home') {
|
||||
endpoint = 'notes/timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
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') {
|
||||
endpoint = 'notes/local-timeline';
|
||||
query = {
|
||||
@@ -95,12 +143,6 @@ if (props.src === 'antenna') {
|
||||
withReplies: props.withReplies,
|
||||
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') {
|
||||
endpoint = 'notes/hybrid-timeline';
|
||||
query = {
|
||||
@@ -108,68 +150,44 @@ if (props.src === 'antenna') {
|
||||
withReplies: props.withReplies,
|
||||
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') {
|
||||
endpoint = 'notes/global-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
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') {
|
||||
endpoint = 'notes/mentions';
|
||||
connection = stream.useChannel('main');
|
||||
connection.on('mention', prepend);
|
||||
} else if (props.src === 'directs') {
|
||||
endpoint = 'notes/mentions';
|
||||
query = {
|
||||
visibility: 'specified',
|
||||
};
|
||||
const onNote = note => {
|
||||
if (note.visibility === 'specified') {
|
||||
prepend(note);
|
||||
}
|
||||
};
|
||||
connection = stream.useChannel('main');
|
||||
connection.on('mention', onNote);
|
||||
} else if (props.src === 'list') {
|
||||
endpoint = 'notes/user-list-timeline';
|
||||
query = {
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
};
|
||||
connection = stream.useChannel('userList', {
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
} else if (props.src === 'channel') {
|
||||
endpoint = 'channels/timeline';
|
||||
query = {
|
||||
channelId: props.channel,
|
||||
};
|
||||
connection = stream.useChannel('channel', {
|
||||
channelId: props.channel,
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
} else if (props.src === 'role') {
|
||||
endpoint = 'roles/notes';
|
||||
query = {
|
||||
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 = {
|
||||
@@ -178,9 +196,19 @@ const pagination = {
|
||||
params: query,
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
connection.dispose();
|
||||
if (connection2) connection2.dispose();
|
||||
const reloadTimeline = (fromPR = false) => {
|
||||
tlNotesCount = 0;
|
||||
|
||||
tlComponent.pagingComponent?.reload().then(() => {
|
||||
reloadStream();
|
||||
if (fromPR) prComponent.refreshFinished();
|
||||
});
|
||||
};
|
||||
|
||||
//const pullRefresh = () => reloadTimeline(true);
|
||||
|
||||
defineExpose({
|
||||
reloadTimeline,
|
||||
});
|
||||
|
||||
/* TODO
|
||||
|
||||
@@ -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[];
|
||||
rootScale?: number;
|
||||
nyaize: boolean | 'account';
|
||||
parsedNodes?: mfm.MfmNode[] | null;
|
||||
};
|
||||
|
||||
// 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
|
||||
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) => {
|
||||
if (t == null) return null;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { App } from 'vue';
|
||||
|
||||
import Mfm from './global/MkMisskeyFlavoredMarkdown.ts';
|
||||
import Mfm from './global/MkMisskeyFlavoredMarkdown.js';
|
||||
import MkA from './global/MkA.vue';
|
||||
import MkAcct from './global/MkAcct.vue';
|
||||
import MkAvatar from './global/MkAvatar.vue';
|
||||
@@ -16,13 +16,14 @@ import MkUserName from './global/MkUserName.vue';
|
||||
import MkEllipsis from './global/MkEllipsis.vue';
|
||||
import MkTime from './global/MkTime.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 MkLoading from './global/MkLoading.vue';
|
||||
import MkError from './global/MkError.vue';
|
||||
import MkAd from './global/MkAd.vue';
|
||||
import MkPageHeader from './global/MkPageHeader.vue';
|
||||
import MkSpacer from './global/MkSpacer.vue';
|
||||
import MkFooterSpacer from './global/MkFooterSpacer.vue';
|
||||
import MkStickyContainer from './global/MkStickyContainer.vue';
|
||||
|
||||
export default function(app: App) {
|
||||
@@ -50,6 +51,7 @@ export const components = {
|
||||
MkAd: MkAd,
|
||||
MkPageHeader: MkPageHeader,
|
||||
MkSpacer: MkSpacer,
|
||||
MkFooterSpacer: MkFooterSpacer,
|
||||
MkStickyContainer: MkStickyContainer,
|
||||
};
|
||||
|
||||
@@ -73,6 +75,7 @@ declare module '@vue/runtime-core' {
|
||||
MkAd: typeof MkAd;
|
||||
MkPageHeader: typeof MkPageHeader;
|
||||
MkSpacer: typeof MkSpacer;
|
||||
MkFooterSpacer: typeof MkFooterSpacer;
|
||||
MkStickyContainer: typeof MkStickyContainer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ export const ROLE_POLICIES = [
|
||||
'inviteLimitCycle',
|
||||
'inviteExpirationTime',
|
||||
'canManageCustomEmojis',
|
||||
'canManageAvatarDecorations',
|
||||
'canSearchNotes',
|
||||
'canUseTranslator',
|
||||
'canHideAds',
|
||||
|
||||
@@ -1061,7 +1061,7 @@
|
||||
["💰", "moneybag", 6],
|
||||
["🪙", "coin", 6],
|
||||
["💳", "credit_card", 6],
|
||||
["🪫", "identification_card", 6],
|
||||
["🪪", "identification_card", 6],
|
||||
["💎", "gem", 6],
|
||||
["⚖", "balance_scale", 6],
|
||||
["🧰", "toolbox", 6],
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { computed, reactive } from 'vue';
|
||||
import { $i } from '@/account.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 * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@@ -19,6 +19,15 @@ export const navbarItemDef = reactive({
|
||||
icon: 'ti ti-bell',
|
||||
show: computed(() => $i != null),
|
||||
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',
|
||||
},
|
||||
drive: {
|
||||
@@ -142,6 +151,13 @@ export const navbarItemDef = reactive({
|
||||
openInstanceMenu(ev);
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
title: i18n.ts.tools,
|
||||
icon: 'ti ti-tool',
|
||||
action: (ev) => {
|
||||
openToolsMenu(ev);
|
||||
},
|
||||
},
|
||||
reload: {
|
||||
title: i18n.ts.reload,
|
||||
icon: 'ti ti-refresh',
|
||||
|
||||
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkInput v-model="ad.url" type="url">
|
||||
<template #label>URL</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.imageUrl">
|
||||
<MkInput v-model="ad.imageUrl" type="url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="ad.place">
|
||||
|
||||
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkTextarea v-model="announcement.text">
|
||||
<template #label>{{ i18n.ts.text }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="announcement.imageUrl">
|
||||
<MkInput v-model="announcement.imageUrl" type="url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="announcement.icon">
|
||||
|
||||
@@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="iconUrl">
|
||||
<MkInput v-model="iconUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="app192IconUrl">
|
||||
<MkInput v-model="app192IconUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
|
||||
<template #caption>
|
||||
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="app512IconUrl">
|
||||
<MkInput v-model="app512IconUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
|
||||
<template #caption>
|
||||
@@ -37,27 +37,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="bannerUrl">
|
||||
<MkInput v-model="bannerUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.bannerUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="backgroundImageUrl">
|
||||
<MkInput v-model="backgroundImageUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="notFoundImageUrl">
|
||||
<MkInput v-model="notFoundImageUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.notFoundDescription }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="infoImageUrl">
|
||||
<MkInput v-model="infoImageUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.nothing }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="serverErrorImageUrl">
|
||||
<MkInput v-model="serverErrorImageUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.somethingHappened }}</template>
|
||||
</MkInput>
|
||||
|
||||
@@ -118,7 +118,7 @@ const menuDef = $computed(() => [{
|
||||
}, {
|
||||
icon: 'ti ti-sparkles',
|
||||
text: i18n.ts.avatarDecorations,
|
||||
to: '/admin/avatar-decorations',
|
||||
to: '/avatar-decorations',
|
||||
active: currentPage?.route.name === 'avatarDecorations',
|
||||
}, {
|
||||
icon: 'ti ti-whirl',
|
||||
|
||||
@@ -20,12 +20,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<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 #label>{{ i18n.ts.tosUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="privacyPolicyUrl">
|
||||
<MkInput v-model="privacyPolicyUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
|
||||
|
||||
<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 #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
|
||||
</MkInput>
|
||||
|
||||
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts.color }}</template>
|
||||
</MkColorInput>
|
||||
|
||||
<MkInput v-model="role.iconUrl">
|
||||
<MkInput v-model="role.iconUrl" type="url">
|
||||
<template #label>{{ i18n.ts._role.iconUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
@@ -259,6 +259,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</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'])">
|
||||
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
||||
<template #suffix>
|
||||
|
||||
@@ -79,6 +79,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkInput>
|
||||
</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'])">
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
|
||||
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
|
||||
<MkInput v-model="impressumUrl">
|
||||
<MkInput v-model="impressumUrl" type="url">
|
||||
<template #label>{{ i18n.ts.impressumUrl }}</template>
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #caption>{{ i18n.ts.impressumDescription }}</template>
|
||||
|
||||
+1
-2
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps">
|
||||
<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>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
@@ -151,6 +151,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch>
|
||||
<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
|
||||
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
|
||||
<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
|
||||
</div>
|
||||
<MkSelect v-model="serverDisconnectedBehavior">
|
||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||
@@ -253,6 +254,7 @@ const notificationPosition = computed(defaultStore.makeGetterSetter('notificatio
|
||||
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
|
||||
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
|
||||
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
|
||||
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
|
||||
|
||||
watch(lang, () => {
|
||||
miLocalStorage.setItem('lang', lang.value as string);
|
||||
@@ -289,6 +291,7 @@ watch([
|
||||
reactionsDisplaySize,
|
||||
highlightSensitiveMedia,
|
||||
keepScreenOn,
|
||||
disableStreamingTimeline,
|
||||
], async () => {
|
||||
await reloadAsk();
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkFooterSpacer/>
|
||||
</mkstickycontainer>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -74,11 +74,11 @@ let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage
|
||||
const userLists = await os.api('users/lists/list');
|
||||
|
||||
async function readAllUnreadNotes() {
|
||||
await os.api('i/read-all-unread-notes');
|
||||
await os.apiWithDialog('i/read-all-unread-notes');
|
||||
}
|
||||
|
||||
async function readAllNotifications() {
|
||||
await os.api('notifications/mark-all-as-read');
|
||||
await os.apiWithDialog('notifications/mark-all-as-read');
|
||||
}
|
||||
|
||||
async function updateReceiveConfig(type, value) {
|
||||
|
||||
@@ -44,6 +44,7 @@ import { $i } from '@/account.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { antennasCache, userListsCache } from '@/cache.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
|
||||
@@ -139,27 +140,42 @@ function focus(): void {
|
||||
tlComponent.focus();
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'ti ti-dots',
|
||||
text: i18n.ts.options,
|
||||
handler: (ev) => {
|
||||
os.popupMenu([{
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRenotes,
|
||||
icon: 'ti ti-repeat',
|
||||
ref: $$(withRenotes),
|
||||
}, src === 'local' || src === 'social' ? {
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||
ref: $$(withReplies),
|
||||
} : undefined, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.fileAttachedOnly,
|
||||
icon: 'ti ti-photo',
|
||||
ref: $$(onlyFiles),
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
}]);
|
||||
const headerActions = $computed(() => {
|
||||
const tmp = [
|
||||
{
|
||||
icon: 'ti ti-dots',
|
||||
text: i18n.ts.options,
|
||||
handler: (ev) => {
|
||||
os.popupMenu([{
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRenotes,
|
||||
icon: 'ti ti-repeat',
|
||||
ref: $$(withRenotes),
|
||||
}, src === 'local' || src === 'social' ? {
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||
ref: $$(withReplies),
|
||||
} : undefined, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.fileAttachedOnly,
|
||||
icon: 'ti ti-photo',
|
||||
ref: $$(onlyFiles),
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
},
|
||||
];
|
||||
if (deviceKind === 'desktop') {
|
||||
tmp.unshift({
|
||||
icon: 'ti ti-refresh',
|
||||
text: i18n.ts.reload,
|
||||
handler: (ev: Event) => {
|
||||
console.log('called');
|
||||
tlComponent.reloadTimeline();
|
||||
},
|
||||
});
|
||||
}
|
||||
return tmp;
|
||||
});
|
||||
|
||||
const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({
|
||||
key: 'list:' + l.id,
|
||||
|
||||
@@ -313,6 +313,10 @@ export const routes = [{
|
||||
}, {
|
||||
path: '/custom-emojis-manager',
|
||||
component: page(() => import('./pages/custom-emojis-manager.vue')),
|
||||
}, {
|
||||
path: '/avatar-decorations',
|
||||
name: 'avatarDecorations',
|
||||
component: page(() => import('./pages/avatar-decorations.vue')),
|
||||
}, {
|
||||
path: '/registry/keys/system/:path(*)?',
|
||||
component: page(() => import('./pages/registry.keys.vue')),
|
||||
@@ -350,7 +354,7 @@ export const routes = [{
|
||||
}, {
|
||||
path: '/avatar-decorations',
|
||||
name: 'avatarDecorations',
|
||||
component: page(() => import('./pages/admin/avatar-decorations.vue')),
|
||||
component: page(() => import('./pages/avatar-decorations.vue')),
|
||||
}, {
|
||||
path: '/queue',
|
||||
name: 'queue',
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { extractUrlFromMfm } from './extract-url-from-mfm.js';
|
||||
|
||||
export function shouldCollapsed(note: Misskey.entities.Note): boolean {
|
||||
const urls = note.text ? extractUrlFromMfm(mfm.parse(note.text)) : null;
|
||||
export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean {
|
||||
const collapsed = note.cw == null && note.text != null && (
|
||||
(note.text.includes('$[x2')) ||
|
||||
(note.text.includes('$[x3')) ||
|
||||
@@ -17,7 +14,7 @@ export function shouldCollapsed(note: Misskey.entities.Note): boolean {
|
||||
(note.text.split('\n').length > 9) ||
|
||||
(note.text.length > 500) ||
|
||||
(note.files.length >= 5) ||
|
||||
(!!urls && urls.length >= 4)
|
||||
(urls.length >= 4)
|
||||
);
|
||||
|
||||
return collapsed;
|
||||
|
||||
@@ -71,9 +71,11 @@ export class WorkerMultiDispatch<POST = any, RETURN = any> {
|
||||
public isTerminated() {
|
||||
return this.terminated;
|
||||
}
|
||||
|
||||
public getWorkers() {
|
||||
return this.workers;
|
||||
}
|
||||
|
||||
public getSymbol() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
@@ -369,6 +369,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'account',
|
||||
default: false,
|
||||
},
|
||||
disableStreamingTimeline: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
@@ -9,6 +9,9 @@ import { $i } from '@/account.js';
|
||||
import { url } from '@/config.js';
|
||||
|
||||
let stream: Misskey.Stream | null = null;
|
||||
let timeoutHeartBeat: number | null = null;
|
||||
|
||||
export let isReloading: boolean = false;
|
||||
|
||||
export function useStream(): Misskey.Stream {
|
||||
if (stream) return stream;
|
||||
@@ -17,7 +20,20 @@ export function useStream(): Misskey.Stream {
|
||||
token: $i.token,
|
||||
} : null));
|
||||
|
||||
window.setTimeout(heartbeat, 1000 * 60);
|
||||
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
export function reloadStream() {
|
||||
if (!stream) return useStream();
|
||||
if (timeoutHeartBeat) window.clearTimeout(timeoutHeartBeat);
|
||||
isReloading = true;
|
||||
|
||||
stream.close();
|
||||
stream.once('_connected_', () => isReloading = false);
|
||||
stream.stream.reconnect();
|
||||
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
|
||||
|
||||
return stream;
|
||||
}
|
||||
@@ -26,5 +42,5 @@ function heartbeat(): void {
|
||||
if (stream != null && document.visibilityState === 'visible') {
|
||||
stream.heartbeat();
|
||||
}
|
||||
window.setTimeout(heartbeat, 1000 * 60);
|
||||
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
|
||||
}
|
||||
|
||||
@@ -155,6 +155,19 @@ hr {
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
._indicateCounter {
|
||||
display: inline-flex;
|
||||
color: var(--fgOnAccent);
|
||||
font-weight: 700;
|
||||
background: var(--indicator);
|
||||
height: 1.5em;
|
||||
min-width: 1.5em;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 99rem;
|
||||
padding: 0.3em 0.5em;
|
||||
}
|
||||
|
||||
._noSelect {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Theme } from '@/scripts/theme.js';
|
||||
import { Theme, getBuiltinThemes } from '@/scripts/theme.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { api } from '@/os.js';
|
||||
import { $i } from '@/account.js';
|
||||
@@ -29,6 +29,10 @@ export async function fetchThemes(): Promise<void> {
|
||||
|
||||
export async function addTheme(theme: Theme): Promise<void> {
|
||||
if ($i == null) return;
|
||||
const builtinThemes = await getBuiltinThemes();
|
||||
if (builtinThemes.some(t => t.id === theme.id)) {
|
||||
throw new Error('builtin theme');
|
||||
}
|
||||
await fetchThemes();
|
||||
const themes = getThemes().concat(theme);
|
||||
await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
|
||||
|
||||
@@ -3,12 +3,42 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import * as os from '@/os.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { host } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
function toolsMenuItems(): MenuItem[] {
|
||||
return [{
|
||||
type: 'link',
|
||||
to: '/scratchpad',
|
||||
text: i18n.ts.scratchpad,
|
||||
icon: 'ti ti-terminal-2',
|
||||
}, {
|
||||
type: 'link',
|
||||
to: '/api-console',
|
||||
text: 'API Console',
|
||||
icon: 'ti ti-terminal-2',
|
||||
}, {
|
||||
type: 'link',
|
||||
to: '/clicker',
|
||||
text: '🍪👈',
|
||||
icon: 'ti ti-cookie',
|
||||
}, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
|
||||
type: 'link',
|
||||
to: '/custom-emojis-manager',
|
||||
text: i18n.ts.manageCustomEmojis,
|
||||
icon: 'ti ti-icons',
|
||||
} : undefined, ($i && ($i.isAdmin || $i.policies.canManageAvatarDecorations)) ? {
|
||||
type: 'link',
|
||||
to: '/avatar-decorations',
|
||||
text: i18n.ts.manageAvatarDecorations,
|
||||
icon: 'ti ti-sparkles',
|
||||
} : undefined];
|
||||
}
|
||||
|
||||
export function openInstanceMenu(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
text: instance.name ?? host,
|
||||
@@ -47,27 +77,7 @@ export function openInstanceMenu(ev: MouseEvent) {
|
||||
type: 'parent',
|
||||
text: i18n.ts.tools,
|
||||
icon: 'ti ti-tool',
|
||||
children: [{
|
||||
type: 'link',
|
||||
to: '/scratchpad',
|
||||
text: i18n.ts.scratchpad,
|
||||
icon: 'ti ti-terminal-2',
|
||||
}, {
|
||||
type: 'link',
|
||||
to: '/api-console',
|
||||
text: 'API Console',
|
||||
icon: 'ti ti-terminal-2',
|
||||
}, {
|
||||
type: 'link',
|
||||
to: '/clicker',
|
||||
text: '🍪👈',
|
||||
icon: 'ti ti-cookie',
|
||||
}, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
|
||||
type: 'link',
|
||||
to: '/custom-emojis-manager',
|
||||
text: i18n.ts.manageCustomEmojis,
|
||||
icon: 'ti ti-icons',
|
||||
} : undefined],
|
||||
children: toolsMenuItems(),
|
||||
}, null, (instance.impressumUrl) ? {
|
||||
text: i18n.ts.impressum,
|
||||
icon: 'ti ti-file-invoice',
|
||||
@@ -100,3 +110,9 @@ export function openInstanceMenu(ev: MouseEvent) {
|
||||
align: 'left',
|
||||
});
|
||||
}
|
||||
|
||||
export function openToolsMenu(ev: MouseEvent) {
|
||||
os.popupMenu(toolsMenuItems(), ev.currentTarget ?? ev.target, {
|
||||
align: 'left',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ let notifications = $ref<Misskey.entities.Notification[]>([]);
|
||||
|
||||
function onNotification(notification: Misskey.entities.Notification, isClient = false) {
|
||||
if (document.visibilityState === 'visible') {
|
||||
if (!isClient) {
|
||||
if (!isClient && notification.type !== 'test') {
|
||||
// サーバーサイドのテスト通知の際は自動で既読をつけない(テストできないので)
|
||||
useStream().send('readNotification');
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-if="item === '-'" :class="$style.divider"></div>
|
||||
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" class="_button" :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" :activeClass="$style.active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
|
||||
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
|
||||
<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
|
||||
<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator">
|
||||
<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
|
||||
<i v-else class="_indicatorCircle"></i>
|
||||
</span>
|
||||
</component>
|
||||
</template>
|
||||
<div :class="$style.divider"></div>
|
||||
@@ -252,6 +255,12 @@ function more() {
|
||||
color: var(--navIndicator);
|
||||
font-size: 8px;
|
||||
animation: blink 1s infinite;
|
||||
|
||||
&:has(.itemIndicateValueIcon) {
|
||||
animation: none;
|
||||
left: auto;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.itemText {
|
||||
|
||||
@@ -29,7 +29,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
|
||||
>
|
||||
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
|
||||
<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
|
||||
<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator">
|
||||
<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
|
||||
<i v-else class="_indicatorCircle"></i>
|
||||
</span>
|
||||
</component>
|
||||
</template>
|
||||
<div :class="$style.divider"></div>
|
||||
@@ -106,7 +109,7 @@ function more(ev: MouseEvent) {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
--nav-width: 250px;
|
||||
--nav-icon-only-width: 72px;
|
||||
--nav-icon-only-width: 80px;
|
||||
|
||||
flex: 0 0 var(--nav-width);
|
||||
width: var(--nav-width);
|
||||
@@ -312,6 +315,13 @@ function more(ev: MouseEvent) {
|
||||
color: var(--navIndicator);
|
||||
font-size: 8px;
|
||||
animation: blink 1s infinite;
|
||||
|
||||
&:has(.itemIndicateValueIcon) {
|
||||
animation: none;
|
||||
left: auto;
|
||||
right: 40px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.itemText {
|
||||
@@ -475,6 +485,14 @@ function more(ev: MouseEvent) {
|
||||
color: var(--navIndicator);
|
||||
font-size: 8px;
|
||||
animation: blink 1s infinite;
|
||||
|
||||
&:has(.itemIndicateValueIcon) {
|
||||
animation: none;
|
||||
top: 4px;
|
||||
left: auto;
|
||||
right: 4px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted } from 'vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { useStream, isReloading } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
@@ -26,6 +26,7 @@ const zIndex = os.claimZIndex('high');
|
||||
let hasDisconnected = $ref(false);
|
||||
|
||||
function onDisconnected() {
|
||||
if (isReloading) return;
|
||||
hasDisconnected = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-if="item === '-'" class="divider"></div>
|
||||
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
|
||||
<i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
|
||||
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_indicatorCircle"></i></span>
|
||||
<span v-if="navbarItemDef[item].indicated" class="indicator">
|
||||
<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
|
||||
<i v-else class="_indicatorCircle"></i>
|
||||
</span>
|
||||
</component>
|
||||
</template>
|
||||
<div class="divider"></div>
|
||||
@@ -218,6 +221,12 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
|
||||
color: var(--navIndicator);
|
||||
font-size: 8px;
|
||||
animation: blink 1s infinite;
|
||||
|
||||
&:has(.itemIndicateValueIcon) {
|
||||
animation: none;
|
||||
left: auto;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -52,7 +52,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-if="isMobile" :class="$style.nav">
|
||||
<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
|
||||
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
|
||||
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"><i :class="$style.navButtonIcon" class="ti ti-bell"></i><span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
|
||||
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')">
|
||||
<i :class="$style.navButtonIcon" class="ti ti-bell"></i>
|
||||
<span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator">
|
||||
<span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button>
|
||||
</div>
|
||||
|
||||
@@ -485,5 +490,10 @@ body {
|
||||
color: var(--indicator);
|
||||
font-size: 16px;
|
||||
animation: blink 1s infinite;
|
||||
|
||||
&:has(.itemIndicateValueIcon) {
|
||||
animation: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,7 +27,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-if="isMobile" ref="navFooter" :class="$style.nav">
|
||||
<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
|
||||
<button :class="$style.navButton" class="_button" @click="mainRouter.currentRoute.value.name === 'index' ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
|
||||
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"><i :class="$style.navButtonIcon" class="ti ti-bell"></i><span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
|
||||
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')">
|
||||
<i :class="$style.navButtonIcon" class="ti ti-bell"></i>
|
||||
<span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator">
|
||||
<span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button :class="$style.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon" class="ti ti-apps"></i></button>
|
||||
<button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button>
|
||||
</div>
|
||||
@@ -319,7 +324,7 @@ $widgets-hide-threshold: 1090px;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
overflow-y: scroll;
|
||||
overscroll-behavior: contain;
|
||||
overscroll-behavior: none;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
@@ -444,6 +449,11 @@ $widgets-hide-threshold: 1090px;
|
||||
color: var(--indicator);
|
||||
font-size: 16px;
|
||||
animation: blink 1s infinite;
|
||||
|
||||
&:has(.itemIndicateValueIcon) {
|
||||
animation: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.menuDrawerBg {
|
||||
|
||||
@@ -2488,6 +2488,7 @@ type MeDetailed = UserDetailed & {
|
||||
hasUnreadMessagingMessage: boolean;
|
||||
hasUnreadNotification: boolean;
|
||||
hasUnreadSpecifiedNotes: boolean;
|
||||
unreadNotificationsCount: number;
|
||||
hideOnlineStatus: boolean;
|
||||
injectFeaturedNote: boolean;
|
||||
integrations: Record<string, any>;
|
||||
@@ -3023,8 +3024,8 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
|
||||
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
||||
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
||||
// src/api.types.ts:633:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||
// src/entities.ts:115:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
||||
// src/entities.ts:611:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||
// src/entities.ts:116:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
||||
// src/entities.ts:612:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
"url": "git+https://github.com/misskey-dev/misskey.js.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/api-extractor": "7.38.0",
|
||||
"@microsoft/api-extractor": "7.38.1",
|
||||
"@swc/jest": "0.2.29",
|
||||
"@types/jest": "29.5.6",
|
||||
"@types/node": "20.8.9",
|
||||
"@typescript-eslint/eslint-plugin": "6.9.0",
|
||||
"@typescript-eslint/parser": "6.9.0",
|
||||
"@types/jest": "29.5.7",
|
||||
"@types/node": "20.8.10",
|
||||
"@typescript-eslint/eslint-plugin": "6.9.1",
|
||||
"@typescript-eslint/parser": "6.9.1",
|
||||
"eslint": "8.52.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
|
||||
@@ -106,6 +106,7 @@ export type MeDetailed = UserDetailed & {
|
||||
hasUnreadMessagingMessage: boolean;
|
||||
hasUnreadNotification: boolean;
|
||||
hasUnreadSpecifiedNotes: boolean;
|
||||
unreadNotificationsCount: number;
|
||||
hideOnlineStatus: boolean;
|
||||
injectFeaturedNote: boolean;
|
||||
integrations: Record<string, any>;
|
||||
|
||||
@@ -72,6 +72,16 @@ module.exports = {
|
||||
{ 'blankLine': 'always', 'prev': 'function', 'next': '*' },
|
||||
{ 'blankLine': 'always', 'prev': '*', 'next': 'function' },
|
||||
],
|
||||
"lines-between-class-members": "off",
|
||||
/* typescript-eslint では enforce に対応してないっぽい
|
||||
'@typescript-eslint/lines-between-class-members': ['error', {
|
||||
enforce: [{
|
||||
blankLine: 'always',
|
||||
prev: 'method',
|
||||
next: '*',
|
||||
}]
|
||||
}],
|
||||
*/
|
||||
'@typescript-eslint/func-call-spacing': ['error', 'never'],
|
||||
'@typescript-eslint/no-explicit-any': ['warn'],
|
||||
'@typescript-eslint/no-unused-vars': ['warn'],
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"misskey-js": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "6.9.0",
|
||||
"@typescript-eslint/parser": "6.9.1",
|
||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
||||
"eslint": "8.52.0",
|
||||
"eslint-plugin-import": "2.29.0",
|
||||
|
||||
@@ -225,6 +225,13 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||
data,
|
||||
}];
|
||||
|
||||
case 'test':
|
||||
return [t('_notification.testNotification'), {
|
||||
body: t('_notification.notificationWillBeDisplayedLikeThis'),
|
||||
badge: iconUrl('bell'),
|
||||
data,
|
||||
}];
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export type BadgeNames =
|
||||
| 'antenna'
|
||||
| 'arrow-back-up'
|
||||
| 'at'
|
||||
| 'bell'
|
||||
| 'chart-arrows'
|
||||
| 'circle-check'
|
||||
| 'medal'
|
||||
|
||||
Generated
+753
-657
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user