Compare commits

..

49 Commits

Author SHA1 Message Date
syuilo
364efbe58b 2023.12.0-beta.4 2023-12-14 13:22:54 +09:00
syuilo
aedc1d0ee9 update deps 2023-12-14 13:22:12 +09:00
1STEP621
b33fe53047 Enhance(frontend): MFMや絵文字が使える入力ボックスでオートコンプリートを使えるように (#12643)
* rich autocomplete for use in profiles, announcements, and channel descriptions

* implementation omissions

* add tab, apply to page editor, and fix something

* componentization

* fix nyaize doesn't working in profile preview

* detach autocomplete instance when unmounted

* fix: mismatched camelCase

* remove unused / unnecessary styles

* update CHANGELOG.md

* fix lint

* remove dump.rdb

* props.richAutocomplete -> autocomplete

* Update packages/frontend/src/scripts/autocomplete.ts

* clarify namings
メンションなども「MFM」に含まれるのか自信がなかったのでrichSyntaxなどとぼかしていましたが、含むようなので変更しました

* tweak

* Update MkFormDialog.vue

* rename

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-12-14 13:11:23 +09:00
おさむのひと
5cee481083 refactor(frontend) $i の型情報にtokenを追加 (#12649) 2023-12-14 12:26:02 +09:00
syuilo
2cfe64e9e6 fix swcrc 2023-12-14 11:35:17 +09:00
syuilo
fbf1f74e12 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-12-14 11:29:29 +09:00
syuilo
839b7483ac enhance(frontend): 同じ種類のデコレーションを複数付けられるように 2023-12-14 11:29:27 +09:00
おさむのひと
eeed67ecac (fix) デフォルト表示時のヘッダにあるチャンネルボタンが反応しない現象の修正 (#12648)
* dividerの仕変に追従

* fix type
2023-12-14 07:18:29 +09:00
syuilo
37820ad572 fix(backend): モデレーションログがモデレーターは閲覧できないように修正
Fix #12622
2023-12-13 18:31:32 +09:00
syuilo
2b3a5f16d1 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-12-13 18:21:20 +09:00
syuilo
17f894348f fix(client): fix glitch when attach/detach avatar decoration 2023-12-13 18:21:17 +09:00
かっこかり
71bb181472 fix(frontend): MkAnimBgをリサイズに対応させる (#12642)
* (fix) MkAnimBgをリサイズに対応させる

* fix lint

* refactor
2023-12-13 18:14:43 +09:00
syuilo
5472f4b934 enhance: アイコンデコレーションを複数設定できるように 2023-12-13 16:56:19 +09:00
YAVIIGI
daea5a39ad fix(frontend): ノート中の絵文字をタップして「リアクションする」を押したときにリアクションサウンドが鳴るようにする (#12624)
* Add sound.play() in copy reaction

* Update CHANGELOG.md

* fix lint error
2023-12-13 08:15:25 +09:00
Camilla Ett
06ca63f9c2 Fix(backend): inboxJobPerSecのデフォルト値を16から32に (#12631) 2023-12-13 08:14:34 +09:00
Tassoman
aad573a1d7 adding color-scheme light to WidgetAichan (#12638) 2023-12-13 08:13:03 +09:00
1STEP621
7f85d7a1f9 Enhance(frontend): リスト/アンテナ/チャンネルをタイムラインから新規作成できるように (#12629)
* add short leads to lists, antennas, and channels

* remove unused import

* add CHANGELOG.md

* hide separator when there is no item

* fix mistakes

* Update timeline.vue

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-12-12 12:19:49 +09:00
syuilo
564a23c0b5 fix type 2023-12-12 10:34:08 +09:00
syuilo
b691126bff refactor(frontend): menuのdividerをnullで表現するのをやめる 2023-12-12 10:26:37 +09:00
woxtu
ebdb443180 Fix trailing commas (#12628) 2023-12-11 20:31:23 +09:00
Acid Chicken (硫酸鶏)
2217d0c050 refactor(frontend): remove redundant class names (#12618) 2023-12-10 17:53:38 +09:00
woxtu
4c135a5ca1 Fix indentation (#12615) 2023-12-10 02:11:17 +09:00
おさむのひと
025afe88b4 (dev) index.htmlにmeta[name=viewport]がなかったので追加&足りてないCSP追加 (#12613)
* (dev) index.htmlにmeta[name=viewport]がなかったので追加&足りてないCSP追加

* fix tab
2023-12-09 21:52:25 +09:00
syuilo
b72f9186b5 2023.12.0-beta.3 2023-12-09 13:15:30 +09:00
Yuriha
dd332b3515 Misskey Playのノート投稿画面で「内容を隠す」を設定できるようにする (#12576)
* Add the content warning option in AiScript UI postFormButton

* Fix initial CW in postFormButton

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-12-09 13:14:51 +09:00
おさむのひと
b7bdd45dba Fix/vue import error on intellij (#12612)
* Fix fix labeler config (#8)

* fix vue import error

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-12-09 13:13:31 +09:00
syuilo
319267e096 update deps 2023-12-09 13:02:14 +09:00
syuilo
fcf0f5f6b5 fix(frontend): disable Mk:apiExternal 2023-12-09 12:58:00 +09:00
zyoshoka
6c1f839cbe chore: labelerが治っていなかったのを修正 (#12610)
* fix ci

* fix

* fix labeler.yml

* Revert "fix labeler.yml"

This reverts commit 9b6a7d02cd.

---------

Co-authored-by: samunohito <46447427+samunohito@users.noreply.github.com>
2023-12-09 09:54:43 +09:00
かっこかり
2c6fc0ba63 fix(dev-frontend) 足りてないCSPを追加 (#12606)
* fix(dev-frontend) サーバーサイドのHTMLと噛み合わない部分を修正

* cspをなおした

* typo
2023-12-08 20:16:49 +09:00
zyoshoka
d10048edac chore: fix labeler's config (#12609) 2023-12-08 20:16:15 +09:00
まっちゃとーにゅ
ab5d2eca1f enhance(frontend): window.openやaタグにnoopenerオプションをつける (MisskeyIO#283) 2023-12-08 19:46:25 +09:00
かっこかり
c54d1cdde2 fix(dev-frontend) サーバーサイドのHTMLと噛み合わない部分を修正 (#12605) 2023-12-08 16:54:33 +09:00
おさむのひと
712e5447b8 Merge pull request #12604 from kakkokari-gtyih/fix-dev-0
fix(dev-frontend) 一部のアセットが読み込まれない問題を修正
2023-12-08 16:37:41 +09:00
kakkokari-gtyih
b760db13bc fix(dev) 一部のアセットが読み込まれない問題を修正 2023-12-08 16:32:24 +09:00
ikasoba
e38af60fd0 fix: secure: true なエンドポイントの型が misskey-js に含まれていない (#12603)
* 作った

* 修正

* 修正
2023-12-08 15:15:17 +09:00
かっこかり
ac4089f37d enhance(frontend): ウィジェットを非表示にできるPageMetaを追加 (#12456)
* (enhance) ウィジェットを非表示にできるPageMetaを追加

* fix lint

* rename

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-12-08 13:06:42 +09:00
dependabot[bot]
f80ae7f686 chore(deps): bump actions/labeler from 4 to 5 (#12584)
Bumps [actions/labeler](https://github.com/actions/labeler) from 4 to 5.
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](https://github.com/actions/labeler/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/labeler
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-08 09:00:37 +09:00
おさむのひと
9059b837fa fix CONTRIBUTING.md (#12600) 2023-12-08 09:00:23 +09:00
おさむのひと
b0039f0946 chore: 開発モードでフロントエンドとバックエンドを独立して起動するようにする(再) (#12593)
* [wip]run standalone vite

* [wip]run standalone vite

* some fix (tabler icons, sw, streaming)

* fix theme

* fix run scripts

* favicon

* client-assets

* cssの読み込み順序とCSP設定の変更

* fix lang change

* fix clientManifest

* baseを相対パスにしてドメイン直下とサブディレクトリ配下両方に対応

* 色々修正

* 色々修正

* 色々修正

* fix

* Revert "client-assets"

This reverts commit 582601e90e.

# Conflicts:
#	packages/frontend/vite.config.ts

* 色々修正

* fix

* fix

* add url and proxy to server proxy

* Update packages/frontend/src/index.html

* wip

* Merge remote-tracking branch 'origin/develop' into feat/launch-standalone-frontend

# Conflicts:
#	packages/frontend/src/pages/welcome.entrance.a.vue

* Merge remote-tracking branch 'origin/develop' into feat/launch-standalone-frontend

# Conflicts:
#	packages/frontend/src/pages/welcome.entrance.a.vue

* fix tabler load

* Apply suggestions from code review

* Update packages/frontend/src/index.html

* fix

* fix vite.config.local-dev.ts

* fix CONTRIBUTING.md

---------

Co-authored-by: FruitRiin <nassii74@gmail.com>
Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
Co-authored-by: 果物リン <fruitriin@riinswork.space>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Co-authored-by: ozelot <contact@ozelot.dev>
2023-12-08 08:22:08 +09:00
KanariKanaru
e6d01e33e6 fix(backend): ブロックした相手から自分のノートが見えないように(/users/featured-notes, /users/notes) (#12511)
* fix: ブロックした相手から自分のノートが見えないように(ユーザー,チャンネル)

* Update CHANGELOG.md

* /users/featured-notesでもブロックを考慮するように

* cacheServiceを使うように

* /channels/timeline.tsで必要のないnoteFilterを持たないように

* Update CHANGELOG.md

* FanoutTimelineEndpointServiceへの対応

- ブロックされている場合は、/users/notesでノートが表示されない
- ミュートしている場合は、ノートが表示される
2023-12-07 18:15:38 +09:00
KanariKanaru
bcf6b7f5ee enhance: meilisearchを有効にしてもミュートやブロックを考慮するように (#12575)
* enhance: meilisearchを有効にしてもミュートやブロックを考慮するように

* Update CHANGELOG.md
2023-12-07 17:09:31 +09:00
anatawa12
1d3ef7b42f fix(backend): pagination with sinceId broken (#12586)
* fix(backend): pagination with sinceId broken

* fix(backend): pagination with sinceId broken for dbFallback
2023-12-07 17:07:06 +09:00
Ryan He
e926411812 chore: Add descriptions for "MeiliSearch" and "allowedPrivateNetworks" to example.yml (#12594)
* Update example.yml, add descriptions for some items

Add descriptions for "MeiliSearch" and "allowedPrivateNetworks"

* Update docker_example.yml

Add descriptions for "MeiliSearch" and "allowedPrivateNetworks"
2023-12-07 17:00:34 +09:00
zyoshoka
406b4bdbe7 refactor(frontend): 非推奨となったReactivity Transformを使わないように (#12539)
* refactor(frontend): 非推奨となったReactivity Transformを使わないように

* refactor: 不要な括弧を除去

* fix: 不要なアノテーションを除去

* fix: Refの配列をrefしている部分の対応

* refactor: 不要な括弧を除去

* fix: lint

* refactor: Ref、ShallowRef、ComputedRefの変数の宣言をletからconstに置換

* fix: type error

* chore: drop reactivity transform from eslint configuration

* refactor: remove unnecessary import

* fix: 対応漏れ
2023-12-07 14:42:09 +09:00
yupix
e42c91dee7 feat: Roleに関するSchemaを追加 (#12572)
* feat: Roleに関連するschemaを追加

* feat: 新しいRoleSchemaを使うように

* chore: misskey.jsのデータを更新

* chore: misskey-js.api.mdを更新
2023-12-06 15:47:57 +09:00
anatawa12
00b11b1f75 chore: hide thumbnail if website is sensitive (#12581) 2023-12-06 13:46:10 +09:00
Yuriha
ad60e43ae4 タイムラインの「リノートを表示」のトグルスイッチが反応しない問題を直す (#12577)
* [frontend] Fix renote toggle switch

* Fix MkMenu rather than usage
2023-12-06 12:07:53 +09:00
anatawa12
8866c530c4 fix(backend): エポックを固定することで年越し時にトレンドが壊れる問題を修正 (#12567) 2023-12-04 20:33:11 +09:00
365 changed files with 12324 additions and 4964 deletions

View File

@@ -56,17 +56,17 @@ dbReplications: false
# You can configure any number of replicas here # You can configure any number of replicas here
#dbSlaves: #dbSlaves:
# - # -
# host: # host:
# port: # port:
# db: # db:
# user: # user:
# pass: # pass:
# - # -
# host: # host:
# port: # port:
# db: # db:
# user: # user:
# pass: # pass:
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Redis configuration └───────────────────────────────────── #───┘ Redis configuration └─────────────────────────────────────
@@ -106,12 +106,16 @@ redis:
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────
# You can set scope to local (default value) or global
# (include notes from remote).
#meilisearch: #meilisearch:
# host: meilisearch # host: meilisearch
# port: 7700 # port: 7700
# apiKey: '' # apiKey: ''
# ssl: true # ssl: true
# index: '' # index: ''
# scope: local
# ┌───────────────┐ # ┌───────────────┐
#───┘ ID generation └─────────────────────────────────────────── #───┘ ID generation └───────────────────────────────────────────
@@ -147,7 +151,7 @@ id: 'aidx'
# Job rate limiter # Job rate limiter
# deliverJobPerSec: 128 # deliverJobPerSec: 128
# inboxJobPerSec: 16 # inboxJobPerSec: 32
# Job attempts # Job attempts
# deliverJobMaxAttempts: 12 # deliverJobMaxAttempts: 12
@@ -180,6 +184,9 @@ proxyRemoteFiles: true
# Sign to ActivityPub GET request (default: true) # Sign to ActivityPub GET request (default: true)
signToActivityPubGet: true signToActivityPubGet: true
# For security reasons, uploading attachments from the intranet is prohibited,
# but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
#allowedPrivateNetworks: [ #allowedPrivateNetworks: [
# '127.0.0.1/32' # '127.0.0.1/32'
#] #]

View File

@@ -118,6 +118,9 @@ redis:
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────
# You can set scope to local (default value) or global
# (include notes from remote).
#meilisearch: #meilisearch:
# host: localhost # host: localhost
# port: 7700 # port: 7700
@@ -163,7 +166,7 @@ id: 'aidx'
# Job rate limiter # Job rate limiter
#deliverJobPerSec: 128 #deliverJobPerSec: 128
#inboxJobPerSec: 16 #inboxJobPerSec: 32
#relashionshipJobPerSec: 64 #relashionshipJobPerSec: 64
# Job attempts # Job attempts
@@ -210,6 +213,9 @@ proxyRemoteFiles: true
# Sign to ActivityPub GET request (default: true) # Sign to ActivityPub GET request (default: true)
signToActivityPubGet: true signToActivityPubGet: true
# For security reasons, uploading attachments from the intranet is prohibited,
# but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
#allowedPrivateNetworks: [ #allowedPrivateNetworks: [
# '127.0.0.1/32' # '127.0.0.1/32'
#] #]

View File

@@ -56,17 +56,17 @@ dbReplications: false
# You can configure any number of replicas here # You can configure any number of replicas here
#dbSlaves: #dbSlaves:
# - # -
# host: # host:
# port: # port:
# db: # db:
# user: # user:
# pass: # pass:
# - # -
# host: # host:
# port: # port:
# db: # db:
# user: # user:
# pass: # pass:
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Redis configuration └───────────────────────────────────── #───┘ Redis configuration └─────────────────────────────────────
@@ -147,7 +147,7 @@ id: 'aidx'
# Job rate limiter # Job rate limiter
# deliverJobPerSec: 128 # deliverJobPerSec: 128
# inboxJobPerSec: 16 # inboxJobPerSec: 32
# Job attempts # Job attempts
# deliverJobMaxAttempts: 12 # deliverJobMaxAttempts: 12

29
.github/labeler.yml vendored
View File

@@ -1,21 +1,34 @@
'packages/backend': 'packages/backend':
- packages/backend/**/* - any:
- changed-files:
- any-glob-to-any-file: ['packages/backend/**/*']
'packages/backend:test': 'packages/backend:test':
- packages/backend/test/**/* - any:
- changed-files:
- any-glob-to-any-file: ['packages/backend/test/**/*']
'packages/frontend': 'packages/frontend':
- packages/frontend/**/* - any:
- changed-files:
- any-glob-to-any-file: ['packages/frontend/**/*']
'packages/frontend:test': 'packages/frontend:test':
- cypress/**/* - any:
- changed-files:
- any-glob-to-any-file: ['cypress/**/*']
'packages/sw': 'packages/sw':
- packages/sw/**/* - any:
- changed-files:
- any-glob-to-any-file: ['packages/sw/**/*']
'packages/misskey-js': 'packages/misskey-js':
- packages/misskey-js/**/* - any:
- changed-files:
- any-glob-to-any-file: ['packages/misskey-js/**/*']
'packages/misskey-js:test': 'packages/misskey-js:test':
- packages/misskey-js/test/**/* - any:
- packages/misskey-js/test-d/**/* - changed-files:
- any-glob-to-any-file: ['packages/misskey-js/test/**/*', 'packages/misskey-js/test-d/**/*']

View File

@@ -11,6 +11,6 @@ jobs:
pull-requests: write pull-requests: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/labeler@v4 - uses: actions/labeler@v5
with: with:
repo-token: "${{ secrets.GITHUB_TOKEN }}" repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -19,6 +19,7 @@
- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed) - Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
- Feat: TL上からートが見えなくなるワードミュートであるハードミュートを追加 - Feat: TL上からートが見えなくなるワードミュートであるハードミュートを追加
- Enhance: アイコンデコレーションを複数設定できるように
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
### Client ### Client
@@ -35,6 +36,11 @@
- Enhance: ノートプレビューに「内容を隠す」が反映されるように - Enhance: ノートプレビューに「内容を隠す」が反映されるように
- Enhance: データセーバーの適用範囲を個別で設定できるように - Enhance: データセーバーの適用範囲を個別で設定できるように
- 従来のデータセーバーの設定はリセットされます - 従来のデータセーバーの設定はリセットされます
- Enhance: タイムライン上のタブからリスト、アンテナ、チャンネルの管理ページにジャンプできるように
- Enhance: ユーザー名、プロフィール、お知らせ、ページの編集画面でMFMや絵文字のオートコンプリートが使用できるように
- Enhance: プロフィール、お知らせの編集画面でMFMのプレビューを表示できるように
- Feat: センシティブと判断されたウェブサイトのサムネイルをぼかすように
- ウェブサイトをセンシティブと判断する仕組みが動いていないため、summalyProxyを使用しないと機能しません。
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正 - fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367 - Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
- Enhance: 絵文字の詳細ページに記載される情報を追加 - Enhance: 絵文字の詳細ページに記載される情報を追加
@@ -44,9 +50,12 @@
- Fix: 共有機能をサポートしていないブラウザの場合は共有ボタンを非表示にする #11305 - Fix: 共有機能をサポートしていないブラウザの場合は共有ボタンを非表示にする #11305
- Fix: 通知のグルーピング設定を変更してもリロードされるまで表示が変わらない問題を修正 #12470 - Fix: 通知のグルーピング設定を変更してもリロードされるまで表示が変わらない問題を修正 #12470
- Fix: 長い名前のチャンネルにおける投稿フォームの表示が崩れる問題を修正 - Fix: 長い名前のチャンネルにおける投稿フォームの表示が崩れる問題を修正
- Fix: セキュリティ向上のためAiScriptの`Mk:apiExternal`を無効化
- Fix: ノート中の絵文字をタップして「リアクションする」からリアクションした際にリアクションサウンドが鳴らない不具合を修正
### Server ### Server
- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
- Enhance: Meilisearchを有効にした検索で、ユーザーのミュートやブロックを考慮するように
- Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303 - Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303
- Fix: ロールタイムラインが保存されない問題を修正 - Fix: ロールタイムラインが保存されない問題を修正
- Fix: api.jsonの生成ロジックを改善 #12402 - Fix: api.jsonの生成ロジックを改善 #12402
@@ -58,6 +67,9 @@
- Fix: Social/Local/Home Timelineにてインスタンスミュートが効かない問題 - Fix: Social/Local/Home Timelineにてインスタンスミュートが効かない問題
- Fix: ユーザのノート一覧にてインスタンスミュートが効かない問題 - Fix: ユーザのノート一覧にてインスタンスミュートが効かない問題
- Fix: チャンネルのノート一覧にてインスタンスミュートが効かない問題 - Fix: チャンネルのノート一覧にてインスタンスミュートが効かない問題
- Fix: 「みつける」が年越し時に壊れる問題を修正
- Fix: アカウントをブロックした際に、自身のユーザーのページでノートが相手に表示される問題を修正
- Fix: モデレーションログがモデレーターは閲覧できないように修正
## 2023.11.1 ## 2023.11.1

View File

@@ -117,6 +117,10 @@ command.
- Server-side source files and automatically builds them if they are modified. Automatically start the server process(es). - Server-side source files and automatically builds them if they are modified. Automatically start the server process(es).
- Vite HMR (just the `vite` command) is available. The behavior may be different from production. - Vite HMR (just the `vite` command) is available. The behavior may be different from production.
- Service Worker is watched by esbuild. - Service Worker is watched by esbuild.
- The front end can be viewed by accessing `http://localhost:5173`.
- The backend listens on the port configured with `port` in .config/default.yml.
If you have not changed it from the default, it will be "http://localhost:3000".
If "port" in .config/default.yml is set to something other than 3000, you need to change the proxy settings in packages/frontend/vite.config.local-dev.ts.
### Dev Container ### Dev Container
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment. Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.

View File

@@ -77,17 +77,17 @@ dbReplications: false
# You can configure any number of replicas here # You can configure any number of replicas here
#dbSlaves: #dbSlaves:
# - # -
# host: # host:
# port: # port:
# db: # db:
# user: # user:
# pass: # pass:
# - # -
# host: # host:
# port: # port:
# db: # db:
# user: # user:
# pass: # pass:
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Redis configuration └───────────────────────────────────── #───┘ Redis configuration └─────────────────────────────────────
@@ -167,7 +167,7 @@ id: "aidx"
# Job rate limiter # Job rate limiter
# deliverJobPerSec: 128 # deliverJobPerSec: 128
# inboxJobPerSec: 16 # inboxJobPerSec: 32
# Job attempts # Job attempts
# deliverJobMaxAttempts: 12 # deliverJobMaxAttempts: 12

View File

@@ -0,0 +1,42 @@
version: "3"
# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します
services:
redis:
restart: always
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- ./redis:/data
healthcheck:
test: "redis-cli ping"
interval: 5s
retries: 20
db:
restart: always
image: postgres:15-alpine
ports:
- "5432:5432"
env_file:
- .config/docker.env
volumes:
- ./db:/var/lib/postgresql/data
healthcheck:
test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
interval: 5s
retries: 20
# meilisearch:
# restart: always
# image: getmeili/meilisearch:v1.3.4
# environment:
# - MEILI_NO_ANALYTICS=true
# - MEILI_ENV=production
# env_file:
# - .config/meilisearch.env
# volumes:
# - ./meili_data:/meili_data

5
locales/index.d.ts vendored
View File

@@ -264,6 +264,7 @@ export interface Locale {
"removeAreYouSure": string; "removeAreYouSure": string;
"deleteAreYouSure": string; "deleteAreYouSure": string;
"resetAreYouSure": string; "resetAreYouSure": string;
"areYouSure": string;
"saved": string; "saved": string;
"messaging": string; "messaging": string;
"upload": string; "upload": string;
@@ -1160,6 +1161,7 @@ export interface Locale {
"avatarDecorations": string; "avatarDecorations": string;
"attach": string; "attach": string;
"detach": string; "detach": string;
"detachAll": string;
"angle": string; "angle": string;
"flip": string; "flip": string;
"showAvatarDecorations": string; "showAvatarDecorations": string;
@@ -1173,6 +1175,7 @@ export interface Locale {
"doReaction": string; "doReaction": string;
"code": string; "code": string;
"reloadRequiredToApplySettings": string; "reloadRequiredToApplySettings": string;
"remainingN": string;
"_announcement": { "_announcement": {
"forExistingUsers": string; "forExistingUsers": string;
"forExistingUsersDescription": string; "forExistingUsersDescription": string;
@@ -1701,6 +1704,7 @@ export interface Locale {
"canHideAds": string; "canHideAds": string;
"canSearchNotes": string; "canSearchNotes": string;
"canUseTranslator": string; "canUseTranslator": string;
"avatarDecorationLimit": string;
}; };
"_condition": { "_condition": {
"isLocal": string; "isLocal": string;
@@ -2181,6 +2185,7 @@ export interface Locale {
"changeAvatar": string; "changeAvatar": string;
"changeBanner": string; "changeBanner": string;
"verifiedLinkDescription": string; "verifiedLinkDescription": string;
"avatarDecorationMax": string;
}; };
"_exportOrImport": { "_exportOrImport": {
"allNotes": string; "allNotes": string;

View File

@@ -261,6 +261,7 @@ removed: "削除しました"
removeAreYouSure: "「{x}」を削除しますか?" removeAreYouSure: "「{x}」を削除しますか?"
deleteAreYouSure: "「{x}」を削除しますか?" deleteAreYouSure: "「{x}」を削除しますか?"
resetAreYouSure: "リセットしますか?" resetAreYouSure: "リセットしますか?"
areYouSure: "よろしいですか?"
saved: "保存しました" saved: "保存しました"
messaging: "チャット" messaging: "チャット"
upload: "アップロード" upload: "アップロード"
@@ -1157,6 +1158,7 @@ tosAndPrivacyPolicy: "利用規約・プライバシーポリシー"
avatarDecorations: "アイコンデコレーション" avatarDecorations: "アイコンデコレーション"
attach: "付ける" attach: "付ける"
detach: "外す" detach: "外す"
detachAll: "全て外す"
angle: "角度" angle: "角度"
flip: "反転" flip: "反転"
showAvatarDecorations: "アイコンのデコレーションを表示" showAvatarDecorations: "アイコンのデコレーションを表示"
@@ -1170,6 +1172,7 @@ cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述
doReaction: "リアクションする" doReaction: "リアクションする"
code: "コード" code: "コード"
reloadRequiredToApplySettings: "設定の反映にはリロードが必要です。" reloadRequiredToApplySettings: "設定の反映にはリロードが必要です。"
remainingN: "残り: {n}"
_announcement: _announcement:
forExistingUsers: "既存ユーザーのみ" forExistingUsers: "既存ユーザーのみ"
@@ -1610,6 +1613,7 @@ _role:
canHideAds: "広告の非表示" canHideAds: "広告の非表示"
canSearchNotes: "ノート検索の利用" canSearchNotes: "ノート検索の利用"
canUseTranslator: "翻訳機能の利用" canUseTranslator: "翻訳機能の利用"
avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
_condition: _condition:
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
isRemote: "リモートユーザー" isRemote: "リモートユーザー"
@@ -2084,6 +2088,7 @@ _profile:
changeAvatar: "アイコン画像を変更" changeAvatar: "アイコン画像を変更"
changeBanner: "バナー画像を変更" changeBanner: "バナー画像を変更"
verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。" verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。"
_exportOrImport: _exportOrImport:
allNotes: "全てのノート" allNotes: "全てのノート"

View File

@@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2023.12.0-beta.2", "version": "2023.12.0-beta.4",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -27,7 +27,7 @@
"check:connect": "cd packages/backend && pnpm check:connect", "check:connect": "cd packages/backend && pnpm check:connect",
"migrateandstart": "pnpm migrate && pnpm start", "migrateandstart": "pnpm migrate && pnpm start",
"watch": "pnpm dev", "watch": "pnpm dev",
"dev": "node ./scripts/dev.mjs", "dev": "pnpm -r dev",
"lint": "pnpm -r lint", "lint": "pnpm -r lint",
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", "cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
"cy:run": "pnpm cypress run", "cy:run": "pnpm cypress run",
@@ -50,13 +50,13 @@
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"postcss": "8.4.32", "postcss": "8.4.32",
"terser": "5.24.0", "terser": "5.24.0",
"typescript": "5.3.2" "typescript": "5.3.3"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "6.13.1", "@typescript-eslint/eslint-plugin": "6.14.0",
"@typescript-eslint/parser": "6.13.1", "@typescript-eslint/parser": "6.14.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.6.0", "cypress": "13.6.1",
"eslint": "8.55.0", "eslint": "8.55.0",
"start-server-and-test": "2.0.3", "start-server-and-test": "2.0.3",
"ncp": "2.0.0" "ncp": "2.0.0"

View File

@@ -11,7 +11,7 @@
"decoratorMetadata": true "decoratorMetadata": true
}, },
"experimental": { "experimental": {
"keepImportAttributes": true "keepImportAssertions": true
}, },
"baseUrl": "src", "baseUrl": "src",
"paths": { "paths": {

View File

@@ -16,6 +16,7 @@
"watch:swc": "swc src -d built -D -w", "watch:swc": "swc src -d built -D -w",
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", "build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
"watch": "node watch.mjs", "watch": "node watch.mjs",
"dev": "node ./built/boot/entry.js",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"eslint": "eslint --quiet \"src/**/*.ts\"", "eslint": "eslint --quiet \"src/**/*.ts\"",
"lint": "pnpm typecheck && pnpm eslint", "lint": "pnpm typecheck && pnpm eslint",
@@ -60,13 +61,13 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.412.0", "@aws-sdk/client-s3": "3.412.0",
"@aws-sdk/lib-storage": "3.412.0", "@aws-sdk/lib-storage": "3.412.0",
"@bull-board/api": "5.10.1", "@bull-board/api": "5.10.2",
"@bull-board/fastify": "5.10.1", "@bull-board/fastify": "5.10.2",
"@bull-board/ui": "5.10.1", "@bull-board/ui": "5.10.2",
"@discordapp/twemoji": "14.1.2", "@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.2.0", "@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.2.0", "@fastify/cookie": "9.2.0",
"@fastify/cors": "8.4.1", "@fastify/cors": "8.4.2",
"@fastify/express": "2.3.0", "@fastify/express": "2.3.0",
"@fastify/http-proxy": "9.3.0", "@fastify/http-proxy": "9.3.0",
"@fastify/multipart": "8.0.0", "@fastify/multipart": "8.0.0",
@@ -88,7 +89,7 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"bullmq": "4.14.4", "bullmq": "4.15.3",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.1", "cbor": "9.0.1",
"chalk": "5.3.0", "chalk": "5.3.0",
@@ -99,7 +100,7 @@
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify": "4.24.3", "fastify": "4.25.0",
"fastify-raw-body": "4.3.0", "fastify-raw-body": "4.3.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.7.0", "file-type": "18.7.0",
@@ -116,7 +117,7 @@
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "23.0.1", "jsdom": "23.0.1",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.3.1", "jsonld": "8.3.2",
"jsrsasign": "10.9.0", "jsrsasign": "10.9.0",
"meilisearch": "0.36.0", "meilisearch": "0.36.0",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
@@ -133,7 +134,7 @@
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2", "oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.2.0", "otpauth": "9.2.1",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.11.3", "pg": "8.11.3",
"pkce-challenge": "4.0.1", "pkce-challenge": "4.0.1",
@@ -147,7 +148,7 @@
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
"re2": "1.20.9", "re2": "1.20.9",
"redis-lock": "0.1.4", "redis-lock": "0.1.4",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.2.0",
"rename": "1.0.4", "rename": "1.0.4",
"rss-parser": "3.13.0", "rss-parser": "3.13.0",
"rxjs": "7.8.1", "rxjs": "7.8.1",
@@ -166,11 +167,11 @@
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.17", "typeorm": "0.3.17",
"typescript": "5.3.2", "typescript": "5.3.3",
"ulid": "2.3.0", "ulid": "2.3.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.6", "web-push": "3.6.6",
"ws": "8.14.2", "ws": "8.15.1",
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
@@ -186,14 +187,14 @@
"@types/content-disposition": "0.5.8", "@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.24", "@types/fluent-ffmpeg": "2.1.24",
"@types/http-link-header": "1.0.5", "@types/http-link-header": "1.0.5",
"@types/jest": "29.5.10", "@types/jest": "29.5.11",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.6", "@types/jsdom": "21.1.6",
"@types/jsonld": "1.5.13", "@types/jsonld": "1.5.13",
"@types/jsrsasign": "10.5.12", "@types/jsrsasign": "10.5.12",
"@types/mime-types": "2.1.4", "@types/mime-types": "2.1.4",
"@types/ms": "0.7.34", "@types/ms": "0.7.34",
"@types/node": "20.10.3", "@types/node": "20.10.4",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.14", "@types/nodemailer": "6.4.14",
"@types/oauth": "0.9.4", "@types/oauth": "0.9.4",
@@ -216,8 +217,8 @@
"@types/vary": "1.1.3", "@types/vary": "1.1.3",
"@types/web-push": "3.6.3", "@types/web-push": "3.6.3",
"@types/ws": "8.5.10", "@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "6.13.1", "@typescript-eslint/eslint-plugin": "6.14.0",
"@typescript-eslint/parser": "6.13.1", "@typescript-eslint/parser": "6.14.0",
"aws-sdk-client-mock": "3.0.0", "aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.55.0", "eslint": "8.55.0",

View File

@@ -28,6 +28,7 @@ type TimelineOptions = {
redisTimelines: FanoutTimelineName[], redisTimelines: FanoutTimelineName[],
noteFilter?: (note: MiNote) => boolean, noteFilter?: (note: MiNote) => boolean,
alwaysIncludeMyNotes?: boolean; alwaysIncludeMyNotes?: boolean;
ignoreAuthorFromBlock?: boolean;
ignoreAuthorFromMute?: boolean; ignoreAuthorFromMute?: boolean;
excludeNoFiles?: boolean; excludeNoFiles?: boolean;
excludeReplies?: boolean; excludeReplies?: boolean;
@@ -60,11 +61,15 @@ export class FanoutTimelineEndpointService {
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える // 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]); if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
const shouldPrepend = ps.sinceId && !ps.untilId;
const idCompare: (a: string, b: string) => number = shouldPrepend ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1;
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId); const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
const redisResultIds = Array.from(new Set(redisResult.flat(1))); const redisResultIds = Array.from(new Set(redisResult.flat(1)));
redisResultIds.sort((a, b) => a > b ? -1 : 1); redisResultIds.sort(idCompare);
noteIds = redisResultIds.slice(0, ps.limit); noteIds = redisResultIds.slice(0, ps.limit);
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0); shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
@@ -109,7 +114,7 @@ export class FanoutTimelineEndpointService {
const parentFilter = filter; const parentFilter = filter;
filter = (note) => { filter = (note) => {
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromMute)) return false; if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false; if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
if (isPureRenote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false; if (isPureRenote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false;
if (isInstanceMuted(note, userMutedInstances)) return false; if (isInstanceMuted(note, userMutedInstances)) return false;
@@ -126,32 +131,43 @@ export class FanoutTimelineEndpointService {
const remainingToRead = ps.limit - redisTimeline.length; const remainingToRead = ps.limit - redisTimeline.length;
// DBからの取り直しを減らす初回と同じ割合以上で成功すると仮定するが、クエリの長さを考えて三倍まで // DBからの取り直しを減らす初回と同じ割合以上で成功すると仮定するが、クエリの長さを考えて三倍まで
const countToGet = remainingToRead * Math.ceil(Math.min(1.1 / lastSuccessfulRate, 3)); const countToGet = Math.ceil(remainingToRead * Math.min(1.1 / lastSuccessfulRate, 3));
noteIds = redisResultIds.slice(readFromRedis, readFromRedis + countToGet); noteIds = redisResultIds.slice(readFromRedis, readFromRedis + countToGet);
readFromRedis += noteIds.length; readFromRedis += noteIds.length;
const gotFromDb = await this.getAndFilterFromDb(noteIds, filter); const gotFromDb = await this.getAndFilterFromDb(noteIds, filter, idCompare);
redisTimeline.push(...gotFromDb); redisTimeline.push(...gotFromDb);
lastSuccessfulRate = gotFromDb.length / noteIds.length; lastSuccessfulRate = gotFromDb.length / noteIds.length;
if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) { if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) {
// 十分Redisからとれた // 十分Redisからとれた
return redisTimeline.slice(0, ps.limit); const result = redisTimeline.slice(0, ps.limit);
if (shouldPrepend) result.reverse();
return result;
} }
} }
// まだ足りない分はDBにフォールバック // まだ足りない分はDBにフォールバック
const remainingToRead = ps.limit - redisTimeline.length; const remainingToRead = ps.limit - redisTimeline.length;
const gotFromDb = await ps.dbFallback(noteIds[noteIds.length - 1], ps.sinceId, remainingToRead); let dbUntil: string | null;
redisTimeline.push(...gotFromDb); let dbSince: string | null;
return redisTimeline; if (shouldPrepend) {
redisTimeline.reverse();
dbUntil = ps.untilId;
dbSince = noteIds[noteIds.length - 1];
} else {
dbUntil = noteIds[noteIds.length - 1];
dbSince = ps.sinceId;
}
const gotFromDb = await ps.dbFallback(dbUntil, dbSince, remainingToRead);
return shouldPrepend ? [...gotFromDb, ...redisTimeline] : [...redisTimeline, ...gotFromDb];
} }
return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit); return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit);
} }
private async getAndFilterFromDb(noteIds: string[], noteFilter: (note: MiNote) => boolean): Promise<MiNote[]> { private async getAndFilterFromDb(noteIds: string[], noteFilter: (note: MiNote) => boolean, idCompare: (a: string, b: string) => number): Promise<MiNote[]> {
const query = this.notesRepository.createQueryBuilder('note') const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds }) .where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
@@ -163,7 +179,7 @@ export class FanoutTimelineEndpointService {
const notes = (await query.getMany()).filter(noteFilter); const notes = (await query.getMany()).filter(noteFilter);
notes.sort((a, b) => a.id > b.id ? -1 : 1); notes.sort((a, b) => idCompare(a.id, b.id));
return notes; return notes;
} }

View File

@@ -14,6 +14,8 @@ export const GALLERY_POSTS_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ご
const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと
const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ごと const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ごと
const featuredEpoc = new Date('2023-01-01T00:00:00Z').getTime();
@Injectable() @Injectable()
export class FeaturedService { export class FeaturedService {
constructor( constructor(
@@ -24,7 +26,7 @@ export class FeaturedService {
@bindThis @bindThis
private getCurrentWindow(windowRange: number): number { private getCurrentWindow(windowRange: number): number {
const passed = new Date().getTime() - new Date(new Date().getFullYear(), 0, 1).getTime(); const passed = new Date().getTime() - featuredEpoc;
return Math.floor(passed / windowRange); return Math.floor(passed / windowRange);
} }

View File

@@ -47,6 +47,7 @@ export type RolePolicies = {
userListLimit: number; userListLimit: number;
userEachUserListsLimit: number; userEachUserListsLimit: number;
rateLimitFactor: number; rateLimitFactor: number;
avatarDecorationLimit: number;
}; };
export const DEFAULT_POLICIES: RolePolicies = { export const DEFAULT_POLICIES: RolePolicies = {
@@ -73,6 +74,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
userListLimit: 10, userListLimit: 10,
userEachUserListsLimit: 50, userEachUserListsLimit: 50,
rateLimitFactor: 1, rateLimitFactor: 1,
avatarDecorationLimit: 1,
}; };
@Injectable() @Injectable()
@@ -326,6 +328,7 @@ export class RoleService implements OnApplicationShutdown {
userListLimit: calc('userListLimit', vs => Math.max(...vs)), userListLimit: calc('userListLimit', vs => Math.max(...vs)),
userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)), userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)), rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
}; };
} }

View File

@@ -12,6 +12,8 @@ import { MiNote } from '@/models/Note.js';
import { MiUser } from '@/models/_.js'; import { MiUser } from '@/models/_.js';
import type { NotesRepository } from '@/models/_.js'; import type { NotesRepository } from '@/models/_.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { CacheService } from '@/core/CacheService.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { Index, MeiliSearch } from 'meilisearch'; import type { Index, MeiliSearch } from 'meilisearch';
@@ -74,6 +76,7 @@ export class SearchService {
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
private cacheService: CacheService,
private queryService: QueryService, private queryService: QueryService,
private idService: IdService, private idService: IdService,
) { ) {
@@ -187,8 +190,19 @@ export class SearchService {
limit: pagination.limit, limit: pagination.limit,
}); });
if (res.hits.length === 0) return []; if (res.hits.length === 0) return [];
const notes = await this.notesRepository.findBy({ const [
userIdsWhoMeMuting,
userIdsWhoBlockingMe,
] = me ? await Promise.all([
this.cacheService.userMutingsCache.fetch(me.id),
this.cacheService.userBlockedCache.fetch(me.id),
]) : [new Set<string>(), new Set<string>()];
const notes = (await this.notesRepository.findBy({
id: In(res.hits.map(x => x.id)), id: In(res.hits.map(x => x.id)),
})).filter(note => {
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
return true;
}); });
return notes.sort((a, b) => a.id > b.id ? -1 : 1); return notes.sort((a, b) => a.id > b.id ? -1 : 1);
} else { } else {

View File

@@ -37,6 +37,7 @@ import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/jso
import { packedFlashSchema } from '@/models/json-schema/flash.js'; import { packedFlashSchema } from '@/models/json-schema/flash.js';
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
import { packedSigninSchema } from '@/models/json-schema/signin.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js';
import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js';
export const refs = { export const refs = {
UserLite: packedUserLiteSchema, UserLite: packedUserLiteSchema,
@@ -73,6 +74,8 @@ export const refs = {
EmojiDetailed: packedEmojiDetailedSchema, EmojiDetailed: packedEmojiDetailedSchema,
Flash: packedFlashSchema, Flash: packedFlashSchema,
Signin: packedSigninSchema, Signin: packedSigninSchema,
RoleLite: packedRoleLiteSchema,
Role: packedRoleSchema,
}; };
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>; export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;

View File

@@ -0,0 +1,158 @@
const rolePolicyValue = {
type: 'object',
properties: {
value: {
oneOf: [
{
type: 'integer',
optional: false, nullable: false,
},
{
type: 'boolean',
optional: false, nullable: false,
},
],
},
priority: {
type: 'integer',
optional: false, nullable: false,
},
useDefault: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;
export const packedRoleLiteSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
name: {
type: 'string',
optional: false, nullable: false,
example: 'New Role',
},
color: {
type: 'string',
optional: false, nullable: true,
example: '#000000',
},
iconUrl: {
type: 'string',
optional: false, nullable: true,
},
description: {
type: 'string',
optional: false, nullable: false,
},
isModerator: {
type: 'boolean',
optional: false, nullable: false,
example: false,
},
isAdministrator: {
type: 'boolean',
optional: false, nullable: false,
example: false,
},
displayOrder: {
type: 'integer',
optional: false, nullable: false,
example: 0,
},
},
} as const;
export const packedRoleSchema = {
type: 'object',
allOf: [
{
type: 'object',
ref: 'RoleLite',
},
{
type: 'object',
properties: {
createdAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
updatedAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
target: {
type: 'string',
optional: false, nullable: false,
enum: ['manual', 'conditional'],
},
condFormula: {
type: 'object',
optional: false, nullable: false,
},
isPublic: {
type: 'boolean',
optional: false, nullable: false,
example: false,
},
isExplorable: {
type: 'boolean',
optional: false, nullable: false,
example: false,
},
asBadge: {
type: 'boolean',
optional: false, nullable: false,
example: false,
},
canEditMembersByModerator: {
type: 'boolean',
optional: false, nullable: false,
example: false,
},
policies: {
type: 'object',
optional: false, nullable: false,
properties: {
pinLimit: rolePolicyValue,
canInvite: rolePolicyValue,
clipLimit: rolePolicyValue,
canHideAds: rolePolicyValue,
inviteLimit: rolePolicyValue,
antennaLimit: rolePolicyValue,
gtlAvailable: rolePolicyValue,
ltlAvailable: rolePolicyValue,
webhookLimit: rolePolicyValue,
canPublicNote: rolePolicyValue,
userListLimit: rolePolicyValue,
wordMuteLimit: rolePolicyValue,
alwaysMarkNsfw: rolePolicyValue,
canSearchNotes: rolePolicyValue,
driveCapacityMb: rolePolicyValue,
rateLimitFactor: rolePolicyValue,
inviteLimitCycle: rolePolicyValue,
noteEachClipsLimit: rolePolicyValue,
inviteExpirationTime: rolePolicyValue,
canManageCustomEmojis: rolePolicyValue,
userEachUserListsLimit: rolePolicyValue,
canManageAvatarDecorations: rolePolicyValue,
canUseTranslator: rolePolicyValue,
avatarDecorationLimit: rolePolicyValue,
},
},
usersCount: {
type: 'integer',
optional: false, nullable: false,
},
},
},
],
} as const;

View File

@@ -329,41 +329,7 @@ export const packedUserDetailedNotMeOnlySchema = {
items: { items: {
type: 'object', type: 'object',
nullable: false, optional: false, nullable: false, optional: false,
properties: { ref: 'RoleLite',
id: {
type: 'string',
nullable: false, optional: false,
format: 'id',
},
name: {
type: 'string',
nullable: false, optional: false,
},
color: {
type: 'string',
nullable: true, optional: false,
},
iconUrl: {
type: 'string',
nullable: true, optional: false,
},
description: {
type: 'string',
nullable: false, optional: false,
},
isModerator: {
type: 'boolean',
nullable: false, optional: false,
},
isAdministrator: {
type: 'boolean',
nullable: false, optional: false,
},
displayOrder: {
type: 'number',
nullable: false, optional: false,
},
},
}, },
}, },
memo: { memo: {
@@ -706,6 +672,10 @@ export const packedMeDetailedOnlySchema = {
type: 'number', type: 'number',
nullable: false, optional: false, nullable: false, optional: false,
}, },
avatarDecorationLimit: {
type: 'number',
nullable: false, optional: false,
},
}, },
}, },
//#region secrets //#region secrets

View File

@@ -226,7 +226,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
autorun: false, autorun: false,
concurrency: this.config.inboxJobConcurrency ?? 16, concurrency: this.config.inboxJobConcurrency ?? 16,
limiter: { limiter: {
max: this.config.inboxJobPerSec ?? 16, max: this.config.inboxJobPerSec ?? 32,
duration: 1000, duration: 1000,
}, },
settings: { settings: {

View File

@@ -13,6 +13,12 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireAdmin: true, requireAdmin: true,
res: {
type: 'object',
optional: false, nullable: false,
ref: 'Role',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@@ -14,6 +14,16 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireModerator: true, requireModerator: true,
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Role',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@@ -23,6 +23,12 @@ export const meta = {
id: '07dc7d34-c0d8-49b7-96c6-db3ce64ee0b3', id: '07dc7d34-c0d8-49b7-96c6-db3ce64ee0b3',
}, },
}, },
res: {
type: 'object',
optional: false, nullable: false,
ref: 'Role',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@@ -14,7 +14,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireModerator: true, requireAdmin: true,
res: { res: {
type: 'array', type: 'array',

View File

@@ -125,7 +125,7 @@ export const meta = {
const muteWords = { type: 'array', items: { oneOf: [ const muteWords = { type: 'array', items: { oneOf: [
{ type: 'array', items: { type: 'string' } }, { type: 'array', items: { type: 'string' } },
{ type: 'string' } { type: 'string' },
] } } as const; ] } } as const;
export const paramDef = { export const paramDef = {
@@ -137,7 +137,7 @@ export const paramDef = {
birthday: { ...birthdaySchema, nullable: true }, birthday: { ...birthdaySchema, nullable: true },
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
avatarId: { type: 'string', format: 'misskey:id', nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true },
avatarDecorations: { type: 'array', maxItems: 1, items: { avatarDecorations: { type: 'array', maxItems: 16, items: {
type: 'object', type: 'object',
properties: { properties: {
id: { type: 'string', format: 'misskey:id' }, id: { type: 'string', format: 'misskey:id' },
@@ -251,7 +251,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
function validateMuteWordRegex(mutedWords: (string[] | string)[]) { function validateMuteWordRegex(mutedWords: (string[] | string)[]) {
for (const mutedWord of mutedWords) { for (const mutedWord of mutedWords) {
if (typeof mutedWord !== "string") continue; if (typeof mutedWord !== 'string') continue;
const regexp = mutedWord.match(/^\/(.+)\/(.*)$/); const regexp = mutedWord.match(/^\/(.+)\/(.*)$/);
if (!regexp) throw new ApiError(meta.errors.invalidRegexp); if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
@@ -329,12 +329,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.avatarDecorations) { if (ps.avatarDecorations) {
const decorations = await this.avatarDecorationService.getAll(true); const decorations = await this.avatarDecorationService.getAll(true);
const myRoles = await this.roleService.getUserRoles(user.id); const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]);
const allRoles = await this.roleService.getRoles(); const allRoles = await this.roleService.getRoles();
const decorationIds = decorations const decorationIds = decorations
.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
.map(d => d.id); .map(d => d.id);
if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
id: d.id, id: d.id,
angle: d.angle ?? 0, angle: d.angle ?? 0,

View File

@@ -13,6 +13,16 @@ export const meta = {
tags: ['role'], tags: ['role'],
requireCredential: true, requireCredential: true,
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Role',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@@ -22,6 +22,12 @@ export const meta = {
id: 'de5502bf-009a-4639-86c1-fec349e46dcb', id: 'de5502bf-009a-4639-86c1-fec349e46dcb',
}, },
}, },
res: {
type: 'object',
optional: false, nullable: false,
ref: 'Role',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@@ -9,6 +9,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { FeaturedService } from '@/core/FeaturedService.js'; import { FeaturedService } from '@/core/FeaturedService.js';
import { CacheService } from '@/core/CacheService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
export const meta = { export const meta = {
tags: ['notes'], tags: ['notes'],
@@ -46,6 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private featuredService: FeaturedService, private featuredService: FeaturedService,
private cacheService: CacheService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
let noteIds = await this.featuredService.getPerUserNotesRanking(ps.userId, 50); let noteIds = await this.featuredService.getPerUserNotesRanking(ps.userId, 50);
@@ -60,6 +63,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return []; return [];
} }
const [
userIdsWhoMeMuting,
userIdsWhoBlockingMe,
] = me ? await Promise.all([
this.cacheService.userMutingsCache.fetch(me.id),
this.cacheService.userBlockedCache.fetch(me.id),
]) : [new Set<string>(), new Set<string>()];
const query = this.notesRepository.createQueryBuilder('note') const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds }) .where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
@@ -69,10 +80,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel'); .leftJoinAndSelect('note.channel', 'channel');
const notes = await query.getMany(); const notes = (await query.getMany()).filter(note => {
notes.sort((a, b) => a.id > b.id ? -1 : 1); if (me && isUserRelated(note, userIdsWhoBlockingMe, false)) return false;
if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false;
// TODO: ミュート等考慮 return true;
});
notes.sort((a, b) => a.id > b.id ? -1 : 1);
return await this.noteEntityService.packMany(notes, me); return await this.noteEntityService.packMany(notes, me);
}); });

View File

@@ -43,7 +43,7 @@ export function genOpenapiSpec(config: Config) {
// 書き換えたりするのでディープコピーしておく。そのまま編集するとメモリ上の値が汚れて次回以降の出力に影響する // 書き換えたりするのでディープコピーしておく。そのまま編集するとメモリ上の値が汚れて次回以降の出力に影響する
const copiedEndpoints = JSON.parse(JSON.stringify(endpoints)) as IEndpoint[]; const copiedEndpoints = JSON.parse(JSON.stringify(endpoints)) as IEndpoint[];
for (const endpoint of copiedEndpoints.filter(ep => !ep.meta.secure)) { for (const endpoint of copiedEndpoints) {
const errors = {} as any; const errors = {} as any;
if (endpoint.meta.errors) { if (endpoint.meta.errors) {
@@ -59,6 +59,11 @@ export function genOpenapiSpec(config: Config) {
const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {};
let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n'; let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n';
if (endpoint.meta.secure) {
desc += '**Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.\n';
}
desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`;
if (endpoint.meta.kind) { if (endpoint.meta.kind) {
const kind = endpoint.meta.kind; const kind = endpoint.meta.kind;

View File

@@ -36,7 +36,7 @@ html
link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl) link(rel='prefetch' href=notFoundImageUrl)
//- https://github.com/misskey-dev/misskey/issues/9842 //- https://github.com/misskey-dev/misskey/issues/9842
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.37.0') link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.44.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`) link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists if !config.clientManifestExists

View File

@@ -69,12 +69,6 @@ module.exports = {
'require': false, 'require': false,
'__dirname': false, '__dirname': false,
// Vue
'$$': false,
'$ref': false,
'$shallowRef': false,
'$computed': false,
// Misskey // Misskey
'_DEV_': false, '_DEV_': false,
'_LANGS_': false, '_LANGS_': false,

View File

@@ -1,6 +1,6 @@
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous"> <link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous">
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous"> <link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.37.0/tabler-icons.min.css"> <link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.44.0/tabler-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css"> <link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
<style> <style>
html { html {

View File

@@ -180,7 +180,7 @@ import './photoswipe-!~{003}~.js';
const _hoisted_1 = createBaseVNode("i", { const _hoisted_1 = createBaseVNode("i", {
class: "ti ti-photo" class: "ti ti-photo"
}, null, -1); }, null, -1);
const _sfc_main = defineComponent({ const index_photos = defineComponent({
__name: "index.photos", __name: "index.photos",
props: { props: {
user: {} user: {}
@@ -261,7 +261,6 @@ const style0 = {
const cssModules = { const cssModules = {
"$style": style0 "$style": style0
}; };
const index_photos = _sfc_main;
export {index_photos as default}; export {index_photos as default};
`.slice(1)); `.slice(1));
}); });

View File

@@ -13,13 +13,13 @@ function isFalsyIdentifier(identifier: estree.Identifier): boolean {
return identifier.name === 'undefined' || identifier.name === 'NaN'; return identifier.name === 'undefined' || identifier.name === 'NaN';
} }
function normalizeClassWalker(tree: estree.Node): string | null { function normalizeClassWalker(tree: estree.Node, stack: string | undefined): string | null {
if (tree.type === 'Identifier') return isFalsyIdentifier(tree) ? '' : null; if (tree.type === 'Identifier') return isFalsyIdentifier(tree) ? '' : null;
if (tree.type === 'Literal') return typeof tree.value === 'string' ? tree.value : ''; if (tree.type === 'Literal') return typeof tree.value === 'string' ? tree.value : '';
if (tree.type === 'BinaryExpression') { if (tree.type === 'BinaryExpression') {
if (tree.operator !== '+') return null; if (tree.operator !== '+') return null;
const left = normalizeClassWalker(tree.left); const left = normalizeClassWalker(tree.left, stack);
const right = normalizeClassWalker(tree.right); const right = normalizeClassWalker(tree.right, stack);
if (left === null || right === null) return null; if (left === null || right === null) return null;
return `${left}${right}`; return `${left}${right}`;
} }
@@ -33,15 +33,15 @@ function normalizeClassWalker(tree: estree.Node): string | null {
if (tree.type === 'ArrayExpression') { if (tree.type === 'ArrayExpression') {
const values = tree.elements.map((treeNode) => { const values = tree.elements.map((treeNode) => {
if (treeNode === null) return ''; if (treeNode === null) return '';
if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument); if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument, stack);
return normalizeClassWalker(treeNode); return normalizeClassWalker(treeNode, stack);
}); });
if (values.some((x) => x === null)) return null; if (values.some((x) => x === null)) return null;
return values.join(' '); return values.join(' ');
} }
if (tree.type === 'ObjectExpression') { if (tree.type === 'ObjectExpression') {
const values = tree.properties.map((treeNode) => { const values = tree.properties.map((treeNode) => {
if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument); if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument, stack);
let x = treeNode.value; let x = treeNode.value;
let inveted = false; let inveted = false;
while (x.type === 'UnaryExpression' && x.operator === '!') { while (x.type === 'UnaryExpression' && x.operator === '!') {
@@ -67,18 +67,26 @@ function normalizeClassWalker(tree: estree.Node): string | null {
if (values.some((x) => x === null)) return null; if (values.some((x) => x === null)) return null;
return values.join(' '); return values.join(' ');
} }
console.error(`Unexpected node type: ${tree.type}`); if (
tree.type !== 'CallExpression' &&
tree.type !== 'ChainExpression' &&
tree.type !== 'ConditionalExpression' &&
tree.type !== 'LogicalExpression' &&
tree.type !== 'MemberExpression') {
console.error(stack ? `Unexpected node type: ${tree.type} (in ${stack})` : `Unexpected node type: ${tree.type}`);
}
return null; return null;
} }
export function normalizeClass(tree: estree.Node): string | null { export function normalizeClass(tree: estree.Node, stack?: string): string | null {
const walked = normalizeClassWalker(tree); const walked = normalizeClassWalker(tree, stack);
return walked && walked.replace(/^\s+|\s+(?=\s)|\s+$/g, ''); return walked && walked.replace(/^\s+|\s+(?=\s)|\s+$/g, '');
} }
export function unwindCssModuleClassName(ast: estree.Node): void { export function unwindCssModuleClassName(ast: estree.Node): void {
(walk as typeof estreeWalker.walk)(ast, { (walk as typeof estreeWalker.walk)(ast, {
enter(node, parent): void { enter(node, parent): void {
//#region
if (parent?.type !== 'Program') return; if (parent?.type !== 'Program') return;
if (node.type !== 'VariableDeclaration') return; if (node.type !== 'VariableDeclaration') return;
if (node.declarations.length !== 1) return; if (node.declarations.length !== 1) return;
@@ -102,6 +110,14 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
return true; return true;
}); });
if (!~__cssModulesIndex) return; if (!~__cssModulesIndex) return;
/* This region assumeed that the entered node looks like the following code.
*
* ```ts
* const SomeComponent = _export_sfc(_sfc_main, [["foo", bar], ["__cssModules", cssModules]]);
* ```
*/
//#endregion
//#region
const cssModuleForestName = ((node.declarations[0].init.arguments[1].elements[__cssModulesIndex] as estree.ArrayExpression).elements[1] as estree.Identifier).name; const cssModuleForestName = ((node.declarations[0].init.arguments[1].elements[__cssModulesIndex] as estree.ArrayExpression).elements[1] as estree.Identifier).name;
const cssModuleForestNode = parent.body.find((x) => { const cssModuleForestNode = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false; if (x.type !== 'VariableDeclaration') return false;
@@ -117,6 +133,16 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
if (property.value.type !== 'Identifier') return []; if (property.value.type !== 'Identifier') return [];
return [[property.key.value as string, property.value.name as string]]; return [[property.key.value as string, property.value.name as string]];
})); }));
/* This region collected a VariableDeclaration node in the module that looks like the following code.
*
* ```ts
* const cssModules = {
* "$style": style0,
* };
* ```
*/
//#endregion
//#region
const sfcMain = parent.body.find((x) => { const sfcMain = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false; if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false; if (x.declarations.length !== 1) return false;
@@ -146,7 +172,22 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
if (ctx.type !== 'Identifier') return; if (ctx.type !== 'Identifier') return;
if (ctx.name !== '_ctx') return; if (ctx.name !== '_ctx') return;
if (render.argument.body.type !== 'BlockStatement') return; if (render.argument.body.type !== 'BlockStatement') return;
/* This region assumed that `sfcMain` looks like the following code.
*
* ```ts
* const _sfc_main = defineComponent({
* setup(_props) {
* ...
* return (_ctx, _cache) => {
* ...
* };
* },
* });
* ```
*/
//#endregion
for (const [key, value] of moduleForest) { for (const [key, value] of moduleForest) {
//#region
const cssModuleTreeNode = parent.body.find((x) => { const cssModuleTreeNode = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false; if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false; if (x.declarations.length !== 1) return false;
@@ -172,6 +213,19 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
if (actualValue.declarations[0].init?.type !== 'Literal') return []; if (actualValue.declarations[0].init?.type !== 'Literal') return [];
return [[actualKey, actualValue.declarations[0].init.value as string]]; return [[actualKey, actualValue.declarations[0].init.value as string]];
})); }));
/* This region collected VariableDeclaration nodes in the module that looks like the following code.
*
* ```ts
* const foo = "bar";
* const baz = "qux";
* const style0 = {
* foo: foo,
* baz: baz,
* };
* ```
*/
//#endregion
//#region
(walk as typeof estreeWalker.walk)(render.argument.body, { (walk as typeof estreeWalker.walk)(render.argument.body, {
enter(childNode) { enter(childNode) {
if (childNode.type !== 'MemberExpression') return; if (childNode.type !== 'MemberExpression') return;
@@ -189,6 +243,39 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
}); });
}, },
}); });
/* This region inlined the reference identifier of the class name in the render function into the actual literal, as in the following code.
*
* ```ts
* const _sfc_main = defineComponent({
* setup(_props) {
* ...
* return (_ctx, _cache) => {
* ...
* return openBlock(), createElementBlock("div", {
* class: normalizeClass(_ctx.$style.foo),
* }, null);
* };
* },
* });
* ```
*
* ↓
*
* ```ts
* const _sfc_main = defineComponent({
* setup(_props) {
* ...
* return (_ctx, _cache) => {
* ...
* return openBlock(), createElementBlock("div", {
* class: normalizeClass("bar"),
* }, null);
* };
* },
* });
*/
//#endregion
//#region
(walk as typeof estreeWalker.walk)(render.argument.body, { (walk as typeof estreeWalker.walk)(render.argument.body, {
enter(childNode) { enter(childNode) {
if (childNode.type !== 'MemberExpression') return; if (childNode.type !== 'MemberExpression') return;
@@ -205,13 +292,47 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
}); });
}, },
}); });
/* This region replaced the reference identifier of missing class names in the render function with `undefined`, as in the following code.
*
* ```ts
* const _sfc_main = defineComponent({
* setup(_props) {
* ...
* return (_ctx, _cache) => {
* ...
* return openBlock(), createElementBlock("div", {
* class: normalizeClass(_ctx.$style.hoge),
* }, null);
* };
* },
* });
* ```
*
* ↓
*
* ```ts
* const _sfc_main = defineComponent({
* setup(_props) {
* ...
* return (_ctx, _cache) => {
* ...
* return openBlock(), createElementBlock("div", {
* class: normalizeClass(undefined),
* }, null);
* };
* },
* });
* ```
*/
//#endregion
//#region
(walk as typeof estreeWalker.walk)(render.argument.body, { (walk as typeof estreeWalker.walk)(render.argument.body, {
enter(childNode) { enter(childNode) {
if (childNode.type !== 'CallExpression') return; if (childNode.type !== 'CallExpression') return;
if (childNode.callee.type !== 'Identifier') return; if (childNode.callee.type !== 'Identifier') return;
if (childNode.callee.name !== 'normalizeClass') return; if (childNode.callee.name !== 'normalizeClass') return;
if (childNode.arguments.length !== 1) return; if (childNode.arguments.length !== 1) return;
const normalized = normalizeClass(childNode.arguments[0]); const normalized = normalizeClass(childNode.arguments[0], name);
if (normalized === null) return; if (normalized === null) return;
this.replace({ this.replace({
type: 'Literal', type: 'Literal',
@@ -219,8 +340,60 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
}); });
}, },
}); });
/* This region compiled the `normalizeClass` call into a pseudo-AOT compilation, as in the following code.
*
* ```ts
* const _sfc_main = defineComponent({
* setup(_props) {
* ...
* return (_ctx, _cache) => {
* ...
* return openBlock(), createElementBlock("div", {
* class: normalizeClass("bar"),
* }, null);
* };
* },
* });
* ```
*
* ↓
*
* ```ts
* const _sfc_main = defineComponent({
* setup(_props) {
* ...
* return (_ctx, _cache) => {
* ...
* return openBlock(), createElementBlock("div", {
* class: "bar",
* }, null);
* };
* },
* });
* ```
*/
//#endregion
} }
//#region
if (node.declarations[0].init.arguments[1].elements.length === 1) { if (node.declarations[0].init.arguments[1].elements.length === 1) {
(walk as typeof estreeWalker.walk)(ast, {
enter(childNode) {
if (childNode.type !== 'Identifier') return;
if (childNode.name !== ident) return;
this.replace({
type: 'Identifier',
name: node.declarations[0].id.name,
});
},
});
this.remove();
/* NOTE: The above logic is valid as long as the following two conditions are met.
*
* - the uniqueness of `ident` is kept throughout the module
* - `_export_sfc` is noop when the second argument is an empty array
*
* Otherwise, the below logic should be used instead.
this.replace({ this.replace({
type: 'VariableDeclaration', type: 'VariableDeclaration',
declarations: [{ declarations: [{
@@ -236,6 +409,7 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
}], }],
kind: 'const', kind: 'const',
}); });
*/
} else { } else {
this.replace({ this.replace({
type: 'VariableDeclaration', type: 'VariableDeclaration',
@@ -263,6 +437,35 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
kind: 'const', kind: 'const',
}); });
} }
/* This region removed the `__cssModules` reference from the second argument of `_export_sfc`, as in the following code.
*
* ```ts
* const SomeComponent = _export_sfc(_sfc_main, [["foo", bar], ["__cssModules", cssModules]]);
* ```
*
* ↓
*
* ```ts
* const SomeComponent = _export_sfc(_sfc_main, [["foo", bar]]);
* ```
*
* When the declaration becomes noop, it is removed as follows.
*
* ```ts
* const _sfc_main = defineComponent({
* ...
* });
* const SomeComponent = _export_sfc(_sfc_main, []);
* ```
*
* ↓
*
* ```ts
* const SomeComponent = defineComponent({
* ...
* });
*/
//#endregion
}, },
}); });
} }

View File

@@ -4,6 +4,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"watch": "vite", "watch": "vite",
"dev": "vite --config vite.config.local-dev.ts",
"build": "vite build", "build": "vite build",
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"", "storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js", "build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
@@ -19,14 +20,13 @@
"@discordapp/twemoji": "14.1.2", "@discordapp/twemoji": "14.1.2",
"@github/webauthn-json": "2.1.1", "@github/webauthn-json": "2.1.1",
"@rollup/plugin-alias": "5.1.0", "@rollup/plugin-alias": "5.1.0",
"@rollup/plugin-json": "6.0.1", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "5.0.5", "@rollup/plugin-replace": "5.0.5",
"@rollup/pluginutils": "5.0.5", "@rollup/pluginutils": "5.1.0",
"@syuilo/aiscript": "0.16.0", "@syuilo/aiscript": "0.16.0",
"@tabler/icons-webfont": "2.37.0", "@tabler/icons-webfont": "2.44.0",
"@vitejs/plugin-vue": "4.5.1", "@vitejs/plugin-vue": "4.5.2",
"@vue-macros/reactivity-transform": "0.4.0", "@vue/compiler-sfc": "3.3.11",
"@vue/compiler-sfc": "3.3.9",
"astring": "1.8.6", "astring": "1.8.6",
"autosize": "6.0.1", "autosize": "6.0.1",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6",
@@ -34,12 +34,12 @@
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"buraha": "0.0.1", "buraha": "0.0.1",
"canvas-confetti": "1.6.1", "canvas-confetti": "1.6.1",
"chart.js": "4.4.0", "chart.js": "4.4.1",
"chartjs-adapter-date-fns": "3.0.0", "chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "2.0.1", "chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1", "chartjs-plugin-zoom": "2.0.1",
"chromatic": "9.1.0", "chromatic": "10.1.0",
"compare-versions": "6.1.0", "compare-versions": "6.1.0",
"cropperjs": "2.0.0-beta.4", "cropperjs": "2.0.0-beta.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
@@ -57,9 +57,9 @@
"photoswipe": "5.4.3", "photoswipe": "5.4.3",
"punycode": "2.3.1", "punycode": "2.3.1",
"querystring": "0.2.1", "querystring": "0.2.1",
"rollup": "4.6.1", "rollup": "4.9.0",
"sanitize-html": "2.11.0", "sanitize-html": "2.11.0",
"shiki": "^0.14.5", "shiki": "0.14.6",
"sass": "1.69.5", "sass": "1.69.5",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
@@ -69,39 +69,39 @@
"tsc-alias": "1.8.8", "tsc-alias": "1.8.8",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typescript": "5.3.2", "typescript": "5.3.3",
"uuid": "9.0.1", "uuid": "9.0.1",
"v-code-diff": "1.7.2", "v-code-diff": "1.7.2",
"vanilla-tilt": "1.8.1", "vanilla-tilt": "1.8.1",
"vite": "5.0.5", "vite": "5.0.8",
"vue": "3.3.9", "vue": "3.3.11",
"vuedraggable": "next" "vuedraggable": "next"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-actions": "7.6.3", "@storybook/addon-actions": "7.6.4",
"@storybook/addon-essentials": "7.6.3", "@storybook/addon-essentials": "7.6.4",
"@storybook/addon-interactions": "7.6.3", "@storybook/addon-interactions": "7.6.4",
"@storybook/addon-links": "7.6.3", "@storybook/addon-links": "7.6.4",
"@storybook/addon-storysource": "7.6.3", "@storybook/addon-storysource": "7.6.4",
"@storybook/addons": "7.6.3", "@storybook/addons": "7.6.4",
"@storybook/blocks": "7.6.3", "@storybook/blocks": "7.6.4",
"@storybook/core-events": "7.6.3", "@storybook/core-events": "7.6.4",
"@storybook/jest": "0.2.3", "@storybook/jest": "0.2.3",
"@storybook/manager-api": "7.6.3", "@storybook/manager-api": "7.6.4",
"@storybook/preview-api": "7.6.3", "@storybook/preview-api": "7.6.4",
"@storybook/react": "7.6.3", "@storybook/react": "7.6.4",
"@storybook/react-vite": "7.6.3", "@storybook/react-vite": "7.6.4",
"@storybook/testing-library": "0.2.2", "@storybook/testing-library": "0.2.2",
"@storybook/theming": "7.6.3", "@storybook/theming": "7.6.4",
"@storybook/types": "7.6.3", "@storybook/types": "7.6.4",
"@storybook/vue3": "7.6.3", "@storybook/vue3": "7.6.4",
"@storybook/vue3-vite": "7.6.3", "@storybook/vue3-vite": "7.6.4",
"@testing-library/vue": "8.0.1", "@testing-library/vue": "8.0.1",
"@types/escape-regexp": "0.0.3", "@types/escape-regexp": "0.0.3",
"@types/estree": "1.0.5", "@types/estree": "1.0.5",
"@types/matter-js": "0.19.5", "@types/matter-js": "0.19.5",
"@types/micromatch": "4.0.6", "@types/micromatch": "4.0.6",
"@types/node": "20.10.3", "@types/node": "20.10.4",
"@types/punycode": "2.1.3", "@types/punycode": "2.1.3",
"@types/sanitize-html": "2.9.5", "@types/sanitize-html": "2.9.5",
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
@@ -109,13 +109,13 @@
"@types/uuid": "9.0.7", "@types/uuid": "9.0.7",
"@types/websocket": "1.0.10", "@types/websocket": "1.0.10",
"@types/ws": "8.5.10", "@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "6.13.1", "@typescript-eslint/eslint-plugin": "6.14.0",
"@typescript-eslint/parser": "6.13.1", "@typescript-eslint/parser": "6.14.0",
"@vitest/coverage-v8": "0.34.6", "@vitest/coverage-v8": "0.34.6",
"@vue/runtime-core": "3.3.9", "@vue/runtime-core": "3.3.11",
"acorn": "8.11.2", "acorn": "8.11.2",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.6.0", "cypress": "13.6.1",
"eslint": "8.55.0", "eslint": "8.55.0",
"eslint-plugin-import": "2.29.0", "eslint-plugin-import": "2.29.0",
"eslint-plugin-vue": "9.19.2", "eslint-plugin-vue": "9.19.2",
@@ -125,17 +125,17 @@
"msw": "1.3.2", "msw": "1.3.2",
"msw-storybook-addon": "1.10.0", "msw-storybook-addon": "1.10.0",
"nodemon": "3.0.2", "nodemon": "3.0.2",
"prettier": "3.1.0", "prettier": "3.1.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"start-server-and-test": "2.0.3", "start-server-and-test": "2.0.3",
"storybook": "7.6.3", "storybook": "7.6.4",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"summaly": "github:misskey-dev/summaly", "summaly": "github:misskey-dev/summaly",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vitest": "0.34.6", "vitest": "0.34.6",
"vitest-fetch-mock": "0.2.2", "vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.3.2", "vue-eslint-parser": "9.3.2",
"vue-tsc": "1.8.24" "vue-tsc": "1.8.25"
} }
} }

View File

@@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
// devモードで起動される際index.htmlを使うときはrouterが暴発してしまってうまく読み込めない。
// よって、devモードとして起動されるときはビルド時に組み込む形としておく。
// (pnpm start時はpugファイルの中で静的リソースとして読み込むようになっており、この問題は起こっていない)
import '@tabler/icons-webfont/tabler-icons.scss';
import('@/_boot_.js');

View File

@@ -16,7 +16,7 @@ import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
type Account = Misskey.entities.MeDetailed; type Account = Misskey.entities.MeDetailed & { token: string };
const accountData = miLocalStorage.getItem('account'); const accountData = miLocalStorage.getItem('account');
@@ -284,7 +284,7 @@ export async function openAccountMenu(opts: {
text: i18n.ts.profile, text: i18n.ts.profile,
to: `/@${ $i.username }`, to: `/@${ $i.username }`,
avatar: $i, avatar: $i,
}, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { }, { type: 'divider' }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
type: 'parent' as const, type: 'parent' as const,
icon: 'ti ti-plus', icon: 'ti ti-plus',
text: i18n.ts.addAccount, text: i18n.ts.addAccount,

View File

@@ -187,6 +187,12 @@ export async function common(createVue: () => App<Element>) {
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme)); if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme)); if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false); defaultStore.set('themeInitial', false);
} else {
if (defaultStore.state.darkMode) {
applyTheme(darkTheme.value);
} else {
applyTheme(lightTheme.value);
}
} }
}); });

View File

@@ -41,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkKeyValue from '@/components/MkKeyValue.vue'; import MkKeyValue from '@/components/MkKeyValue.vue';
@@ -56,11 +57,11 @@ const emit = defineEmits<{
(ev: 'resolved', reportId: string): void; (ev: 'resolved', reportId: string): void;
}>(); }>();
let forward = $ref(props.report.forwarded); const forward = ref(props.report.forwarded);
function resolve() { function resolve() {
os.apiWithDialog('admin/resolve-abuse-user-report', { os.apiWithDialog('admin/resolve-abuse-user-report', {
forward: forward, forward: forward.value,
reportId: props.report.id, reportId: props.report.id,
}).then(() => { }).then(() => {
emit('resolved', props.report.id); emit('resolved', props.report.id);

View File

@@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { onMounted } from 'vue'; import { onMounted, ref, computed } from 'vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js'; import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js';
@@ -67,15 +67,15 @@ const props = withDefaults(defineProps<{
withDescription: true, withDescription: true,
}); });
let achievements = $ref(); const achievements = ref();
const lockedAchievements = $computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements ?? []).some(a => a.name === x))); const lockedAchievements = computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements.value ?? []).some(a => a.name === x)));
function fetch() { function fetch() {
os.api('users/achievements', { userId: props.user.id }).then(res => { os.api('users/achievements', { userId: props.user.id }).then(res => {
achievements = []; achievements.value = [];
for (const t of ACHIEVEMENT_TYPES) { for (const t of ACHIEVEMENT_TYPES) {
const a = res.find(x => x.name === t); const a = res.find(x => x.name === t);
if (a) achievements.push(a); if (a) achievements.value.push(a);
} }
//achievements = res.sort((a, b) => b.unlockedAt - a.unlockedAt); //achievements = res.sort((a, b) => b.unlockedAt - a.unlockedAt);
}); });

View File

@@ -138,45 +138,45 @@ const texts = computed(() => {
}); });
let enabled = true; let enabled = true;
let majorGraduationColor = $ref<string>(); const majorGraduationColor = ref<string>();
//let minorGraduationColor = $ref<string>(); //let minorGraduationColor = $ref<string>();
let sHandColor = $ref<string>(); const sHandColor = ref<string>();
let mHandColor = $ref<string>(); const mHandColor = ref<string>();
let hHandColor = $ref<string>(); const hHandColor = ref<string>();
let nowColor = $ref<string>(); const nowColor = ref<string>();
let h = $ref<number>(0); const h = ref<number>(0);
let m = $ref<number>(0); const m = ref<number>(0);
let s = $ref<number>(0); const s = ref<number>(0);
let hAngle = $ref<number>(0); const hAngle = ref<number>(0);
let mAngle = $ref<number>(0); const mAngle = ref<number>(0);
let sAngle = $ref<number>(0); const sAngle = ref<number>(0);
let disableSAnimate = $ref(false); const disableSAnimate = ref(false);
let sOneRound = false; let sOneRound = false;
const sLine = ref<SVGPathElement>(); const sLine = ref<SVGPathElement>();
function tick() { function tick() {
const now = props.now(); const now = props.now();
now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset); now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset);
const previousS = s; const previousS = s.value;
const previousM = m; const previousM = m.value;
const previousH = h; const previousH = h.value;
s = now.getSeconds(); s.value = now.getSeconds();
m = now.getMinutes(); m.value = now.getMinutes();
h = now.getHours(); h.value = now.getHours();
if (previousS === s && previousM === m && previousH === h) { if (previousS === s.value && previousM === m.value && previousH === h.value) {
return; return;
} }
hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6); hAngle.value = Math.PI * (h.value % (props.twentyfour ? 24 : 12) + (m.value + s.value / 60) / 60) / (props.twentyfour ? 12 : 6);
mAngle = Math.PI * (m + s / 60) / 30; mAngle.value = Math.PI * (m.value + s.value / 60) / 30;
if (sOneRound && sLine.value) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない) if (sOneRound && sLine.value) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない)
sAngle = Math.PI * 60 / 30; sAngle.value = Math.PI * 60 / 30;
defaultIdlingRenderScheduler.delete(tick); defaultIdlingRenderScheduler.delete(tick);
sLine.value.addEventListener('transitionend', () => { sLine.value.addEventListener('transitionend', () => {
disableSAnimate = true; disableSAnimate.value = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
sAngle = 0; sAngle.value = 0;
requestAnimationFrame(() => { requestAnimationFrame(() => {
disableSAnimate = false; disableSAnimate.value = false;
if (enabled) { if (enabled) {
defaultIdlingRenderScheduler.add(tick); defaultIdlingRenderScheduler.add(tick);
} }
@@ -184,9 +184,9 @@ function tick() {
}); });
}, { once: true }); }, { once: true });
} else { } else {
sAngle = Math.PI * s / 30; sAngle.value = Math.PI * s.value / 30;
} }
sOneRound = s === 59; sOneRound = s.value === 59;
} }
tick(); tick();
@@ -195,12 +195,12 @@ function calcColors() {
const computedStyle = getComputedStyle(document.documentElement); const computedStyle = getComputedStyle(document.documentElement);
const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark(); const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark();
const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(); const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
majorGraduationColor = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'; majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; //minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
sHandColor = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)'; sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
mHandColor = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString(); mHandColor.value = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString();
hHandColor = accent; hHandColor.value = accent;
nowColor = accent; nowColor.value = accent;
} }
calcColors(); calcColors();

View File

@@ -21,8 +21,9 @@ const props = withDefaults(defineProps<{
focus: 1.0, focus: 1.0,
}); });
function loadShader(gl, type, source) { function loadShader(gl: WebGLRenderingContext, type: number, source: string) {
const shader = gl.createShader(type); const shader = gl.createShader(type);
if (shader == null) return null;
gl.shaderSource(shader, source); gl.shaderSource(shader, source);
gl.compileShader(shader); gl.compileShader(shader);
@@ -38,11 +39,13 @@ function loadShader(gl, type, source) {
return shader; return shader;
} }
function initShaderProgram(gl, vsSource, fsSource) { function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram(); const shaderProgram = gl.createProgram();
if (shaderProgram == null || vertexShader == null || fragmentShader == null) return null;
gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader); gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram); gl.linkProgram(shaderProgram);
@@ -63,8 +66,10 @@ let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
onMounted(() => { onMounted(() => {
const canvas = canvasEl.value!; const canvas = canvasEl.value!;
canvas.width = canvas.offsetWidth; let width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight; let height = canvas.offsetHeight;
canvas.width = width;
canvas.height = height;
const gl = canvas.getContext('webgl', { premultipliedAlpha: true }); const gl = canvas.getContext('webgl', { premultipliedAlpha: true });
if (gl == null) return; if (gl == null) return;
@@ -197,6 +202,7 @@ onMounted(() => {
gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) ); gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
} }
`); `);
if (shaderProgram == null) return;
gl.useProgram(shaderProgram); gl.useProgram(shaderProgram);
const u_resolution = gl.getUniformLocation(shaderProgram, 'u_resolution'); const u_resolution = gl.getUniformLocation(shaderProgram, 'u_resolution');
@@ -226,7 +232,23 @@ onMounted(() => {
gl!.uniform1f(u_time, 0); gl!.uniform1f(u_time, 0);
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4); gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
} else { } else {
function render(timeStamp) { function render(timeStamp: number) {
let sizeChanged = false;
if (Math.abs(height - canvas.offsetHeight) > 2) {
height = canvas.offsetHeight;
canvas.height = height;
sizeChanged = true;
}
if (Math.abs(width - canvas.offsetWidth) > 2) {
width = canvas.offsetWidth;
canvas.width = width;
sizeChanged = true;
}
if (sizeChanged && gl) {
gl.uniform2fv(u_resolution, [width, height]);
gl.viewport(0, 0, width, height);
}
gl!.uniform1f(u_time, timeStamp); gl!.uniform1f(u_time, timeStamp);
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4); gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);

View File

@@ -43,6 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
fixed fixed
:instant="true" :instant="true"
:initialText="c.form.text" :initialText="c.form.text"
:initialCw="c.form.cw"
/> />
</div> </div>
<MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened"> <MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened">
@@ -60,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Ref } from 'vue'; import { Ref, ref } from 'vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
@@ -87,16 +88,17 @@ function g(id) {
return props.components.find(x => x.value.id === id).value; return props.components.find(x => x.value.id === id).value;
} }
let valueForSwitch = $ref(c.default ?? false); const valueForSwitch = ref(c.default ?? false);
function onSwitchUpdate(v) { function onSwitchUpdate(v) {
valueForSwitch = v; valueForSwitch.value = v;
if (c.onChange) c.onChange(v); if (c.onChange) c.onChange(v);
} }
function openPostForm() { function openPostForm() {
os.post({ os.post({
initialText: c.form.text, initialText: c.form.text,
initialCw: c.form.cw,
instant: true, instant: true,
}); });
} }

View File

@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted } from 'vue'; import { nextTick, onMounted, shallowRef } from 'vue';
const props = defineProps<{ const props = defineProps<{
type?: 'button' | 'submit' | 'reset'; type?: 'button' | 'submit' | 'reset';
@@ -59,13 +59,13 @@ const emit = defineEmits<{
(ev: 'click', payload: MouseEvent): void; (ev: 'click', payload: MouseEvent): void;
}>(); }>();
let el = $shallowRef<HTMLElement | null>(null); const el = shallowRef<HTMLElement | null>(null);
let ripples = $shallowRef<HTMLElement | null>(null); const ripples = shallowRef<HTMLElement | null>(null);
onMounted(() => { onMounted(() => {
if (props.autofocus) { if (props.autofocus) {
nextTick(() => { nextTick(() => {
el!.focus(); el.value!.focus();
}); });
} }
}); });
@@ -88,11 +88,11 @@ function onMousedown(evt: MouseEvent): void {
const rect = target.getBoundingClientRect(); const rect = target.getBoundingClientRect();
const ripple = document.createElement('div'); const ripple = document.createElement('div');
ripple.classList.add(ripples!.dataset.childrenClass!); ripple.classList.add(ripples.value!.dataset.childrenClass!);
ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px'; ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px'; ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';
ripples!.appendChild(ripple); ripples.value!.appendChild(ripple);
const circleCenterX = evt.clientX - rect.left; const circleCenterX = evt.clientX - rect.left;
const circleCenterY = evt.clientY - rect.top; const circleCenterY = evt.clientY - rect.top;
@@ -107,7 +107,7 @@ function onMousedown(evt: MouseEvent): void {
ripple.style.opacity = '0'; ripple.style.opacity = '0';
}, 1000); }, 1000);
window.setTimeout(() => { window.setTimeout(() => {
if (ripples) ripples.removeChild(ripple); if (ripples.value) ripples.value.removeChild(ripple);
}, 2000); }, 2000);
} }
</script> </script>

View File

@@ -74,7 +74,7 @@ const props = defineProps({
}, },
}); });
let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
const negate = arr => arr.map(x => -x); const negate = arr => arr.map(x => -x);
@@ -268,7 +268,7 @@ const render = () => {
gradient, gradient,
}, },
}, },
plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl)] : [])], plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl.value)] : [])],
}); });
}; };

View File

@@ -13,29 +13,30 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { shallowRef } from 'vue';
import { Chart, LegendItem } from 'chart.js'; import { Chart, LegendItem } from 'chart.js';
const props = defineProps({ const props = defineProps({
}); });
let chart = $shallowRef<Chart>(); const chart = shallowRef<Chart>();
let items = $shallowRef<LegendItem[]>([]); const items = shallowRef<LegendItem[]>([]);
function update(_chart: Chart, _items: LegendItem[]) { function update(_chart: Chart, _items: LegendItem[]) {
chart = _chart, chart.value = _chart,
items = _items; items.value = _items;
} }
function onClick(item: LegendItem) { function onClick(item: LegendItem) {
if (chart == null) return; if (chart.value == null) return;
const { type } = chart.config; const { type } = chart.value.config;
if (type === 'pie' || type === 'doughnut') { if (type === 'pie' || type === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item // Pie and doughnut charts only have a single dataset and visibility is per item
chart.toggleDataVisibility(item.index); chart.value.toggleDataVisibility(item.index);
} else { } else {
chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex)); chart.value.setDatasetVisibility(item.datasetIndex, !chart.value.isDatasetVisible(item.datasetIndex));
} }
chart.update(); chart.value.update();
} }
defineExpose({ defineExpose({

View File

@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, onUnmounted } from 'vue'; import { computed, onMounted, onUnmounted, ref } from 'vue';
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js'; import { useInterval } from '@/scripts/use-interval.js';
@@ -29,8 +29,8 @@ import { claimAchievement } from '@/scripts/achievements.js';
const saveData = game.saveData; const saveData = game.saveData;
const cookies = computed(() => saveData.value?.cookies); const cookies = computed(() => saveData.value?.cookies);
let cps = $ref(0); const cps = ref(0);
let prevCookies = $ref(0); const prevCookies = ref(0);
function onClick(ev: MouseEvent) { function onClick(ev: MouseEvent) {
const x = ev.clientX; const x = ev.clientX;
@@ -48,9 +48,9 @@ function onClick(ev: MouseEvent) {
} }
useInterval(() => { useInterval(() => {
const diff = saveData.value!.cookies - prevCookies; const diff = saveData.value!.cookies - prevCookies.value;
cps = diff; cps.value = diff;
prevCookies = saveData.value!.cookies; prevCookies.value = saveData.value!.cookies;
}, 1000, { }, 1000, {
immediate: false, immediate: false,
afterMounted: true, afterMounted: true,
@@ -63,7 +63,7 @@ useInterval(game.save, 1000 * 5, {
onMounted(async () => { onMounted(async () => {
await game.load(); await game.load();
prevCookies = saveData.value!.cookies; prevCookies.value = saveData.value!.cookies;
}); });
onUnmounted(() => { onUnmounted(() => {

View File

@@ -54,7 +54,7 @@ watch(() => props.lang, (to) => {
return new Promise((resolve) => { return new Promise((resolve) => {
fetchLanguage(to).then(() => resolve); fetchLanguage(to).then(() => resolve);
}); });
}, { immediate: true, }); }, { immediate: true });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div :class="[$style.codeEditorRoot, { [$style.disabled]: disabled, [$style.focused]: focused }]"> <div :class="[$style.codeEditorRoot, { [$style.focused]: focused }]">
<div :class="$style.codeEditorScroller"> <div :class="$style.codeEditorScroller">
<textarea <textarea
ref="inputEl" ref="inputEl"

View File

@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onBeforeUnmount } from 'vue'; import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue';
import MkMenu from './MkMenu.vue'; import MkMenu from './MkMenu.vue';
import { MenuItem } from './types/menu.vue'; import { MenuItem } from './types/menu.vue';
import contains from '@/scripts/contains.js'; import contains from '@/scripts/contains.js';
@@ -34,9 +34,9 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
let rootEl = $shallowRef<HTMLDivElement>(); const rootEl = shallowRef<HTMLDivElement>();
let zIndex = $ref<number>(os.claimZIndex('high')); const zIndex = ref<number>(os.claimZIndex('high'));
const SCROLLBAR_THICKNESS = 16; const SCROLLBAR_THICKNESS = 16;
@@ -44,8 +44,8 @@ onMounted(() => {
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
const width = rootEl.offsetWidth; const width = rootEl.value.offsetWidth;
const height = rootEl.offsetHeight; const height = rootEl.value.offsetHeight;
if (left + width - window.pageXOffset >= (window.innerWidth - SCROLLBAR_THICKNESS)) { if (left + width - window.pageXOffset >= (window.innerWidth - SCROLLBAR_THICKNESS)) {
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset; left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset;
@@ -63,8 +63,8 @@ onMounted(() => {
left = 0; left = 0;
} }
rootEl.style.top = `${top}px`; rootEl.value.style.top = `${top}px`;
rootEl.style.left = `${left}px`; rootEl.value.style.left = `${left}px`;
document.body.addEventListener('mousedown', onMousedown); document.body.addEventListener('mousedown', onMousedown);
}); });
@@ -74,7 +74,7 @@ onBeforeUnmount(() => {
}); });
function onMousedown(evt: Event) { function onMousedown(evt: Event) {
if (!contains(rootEl, evt.target) && (rootEl !== evt.target)) emit('closed'); if (!contains(rootEl.value, evt.target) && (rootEl.value !== evt.target)) emit('closed');
} }
</script> </script>

View File

@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted, shallowRef, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import Cropper from 'cropperjs'; import Cropper from 'cropperjs';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
@@ -56,10 +56,10 @@ const props = defineProps<{
}>(); }>();
const imgUrl = getProxiedImageUrl(props.file.url, undefined, true); const imgUrl = getProxiedImageUrl(props.file.url, undefined, true);
let dialogEl = $shallowRef<InstanceType<typeof MkModalWindow>>(); const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
let imgEl = $shallowRef<HTMLImageElement>(); const imgEl = shallowRef<HTMLImageElement>();
let cropper: Cropper | null = null; let cropper: Cropper | null = null;
let loading = $ref(true); const loading = ref(true);
const ok = async () => { const ok = async () => {
const promise = new Promise<Misskey.entities.DriveFile>(async (res) => { const promise = new Promise<Misskey.entities.DriveFile>(async (res) => {
@@ -94,16 +94,16 @@ const ok = async () => {
const f = await promise; const f = await promise;
emit('ok', f); emit('ok', f);
dialogEl!.close(); dialogEl.value!.close();
}; };
const cancel = () => { const cancel = () => {
emit('cancel'); emit('cancel');
dialogEl!.close(); dialogEl.value!.close();
}; };
const onImageLoad = () => { const onImageLoad = () => {
loading = false; loading.value = false;
if (cropper) { if (cropper) {
cropper.getCropperImage()!.$center('contain'); cropper.getCropperImage()!.$center('contain');
@@ -112,7 +112,7 @@ const onImageLoad = () => {
}; };
onMounted(() => { onMounted(() => {
cropper = new Cropper(imgEl!, { cropper = new Cropper(imgEl.value!, {
}); });
const computedStyle = getComputedStyle(document.documentElement); const computedStyle = getComputedStyle(document.documentElement);

View File

@@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown"> <MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template> <template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
<template #caption> <template #caption>
<span v-if="okButtonDisabled && disabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/> <span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
<span v-else-if="okButtonDisabled && disabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/> <span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
</template> </template>
</MkInput> </MkInput>
<MkSelect v-if="select" v-model="selectedValue" autofocus> <MkSelect v-if="select" v-model="selectedValue" autofocus>
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
</MkSelect> </MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons"> <div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabled" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton> <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton> <MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
</div> </div>
<div v-if="actions" :class="$style.buttons"> <div v-if="actions" :class="$style.buttons">
@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'; import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
@@ -122,24 +122,21 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
const inputValue = ref<string | number | null>(props.input?.default ?? null); const inputValue = ref<string | number | null>(props.input?.default ?? null);
const selectedValue = ref(props.select?.default ?? null); const selectedValue = ref(props.select?.default ?? null);
let disabledReason = $ref<null | 'charactersExceeded' | 'charactersBelow'>(null); const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
const okButtonDisabled = $computed<boolean>(() => {
if (props.input) { if (props.input) {
if (props.input.minLength) { if (props.input.minLength) {
if ((inputValue.value || inputValue.value === '') && (inputValue.value as string).length < props.input.minLength) { if ((inputValue.value || inputValue.value === '') && (inputValue.value as string).length < props.input.minLength) {
disabledReason = 'charactersBelow'; return 'charactersBelow';
return true;
} }
} }
if (props.input.maxLength) { if (props.input.maxLength) {
if (inputValue.value && (inputValue.value as string).length > props.input.maxLength) { if (inputValue.value && (inputValue.value as string).length > props.input.maxLength) {
disabledReason = 'charactersExceeded'; return 'charactersExceeded';
return true;
} }
} }
} }
return false; return null;
}); });
function done(canceled: boolean, result?) { function done(canceled: boolean, result?) {

View File

@@ -39,6 +39,7 @@ import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { MenuItem } from '@/types/menu.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
folder: Misskey.entities.DriveFolder; folder: Misskey.entities.DriveFolder;
@@ -250,7 +251,7 @@ function setAsUploadFolder() {
} }
function onContextmenu(ev: MouseEvent) { function onContextmenu(ev: MouseEvent) {
let menu; let menu: MenuItem[];
menu = [{ menu = [{
text: i18n.ts.openInWindow, text: i18n.ts.openInWindow,
icon: 'ti ti-app-window', icon: 'ti ti-app-window',
@@ -260,18 +261,18 @@ function onContextmenu(ev: MouseEvent) {
}, { }, {
}, 'closed'); }, 'closed');
}, },
}, null, { }, { type: 'divider' }, {
text: i18n.ts.rename, text: i18n.ts.rename,
icon: 'ti ti-forms', icon: 'ti ti-forms',
action: rename, action: rename,
}, null, { }, { type: 'divider' }, {
text: i18n.ts.delete, text: i18n.ts.delete,
icon: 'ti ti-trash', icon: 'ti ti-trash',
danger: true, danger: true,
action: deleteFolder, action: deleteFolder,
}]; }];
if (defaultStore.state.devMode) { if (defaultStore.state.devMode) {
menu = menu.concat([null, { menu = menu.concat([{ type: 'divider' }, {
icon: 'ti ti-id', icon: 'ti ti-id',
text: i18n.ts.copyFolderId, text: i18n.ts.copyFolderId,
action: () => { action: () => {

View File

@@ -616,7 +616,7 @@ function getMenu() {
type: 'switch', type: 'switch',
text: i18n.ts.keepOriginalUploading, text: i18n.ts.keepOriginalUploading,
ref: keepOriginal, ref: keepOriginal,
}, null, { }, { type: 'divider' }, {
text: i18n.ts.addFile, text: i18n.ts.addFile,
type: 'label', type: 'label',
}, { }, {
@@ -627,7 +627,7 @@ function getMenu() {
text: i18n.ts.fromUrl, text: i18n.ts.fromUrl,
icon: 'ti ti-link', icon: 'ti ti-link',
action: () => { urlUpload(); }, action: () => { urlUpload(); },
}, null, { }, { type: 'divider' }, {
text: folder.value ? folder.value.name : i18n.ts.drive, text: folder.value ? folder.value.name : i18n.ts.drive,
type: 'label', type: 'label',
}, folder.value ? { }, folder.value ? {

View File

@@ -26,35 +26,35 @@ SPDX-License-Identifier: AGPL-3.0-only
</section> </section>
<!-- フォルダの中にはカスタム絵文字やフォルダがある --> <!-- フォルダの中にはカスタム絵文字やフォルダがある -->
<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);"> <section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
<header class="_acrylic" @click="shown = !shown"> <header class="_acrylic" @click="shown = !shown">
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }}) <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }})
</header> </header>
<div v-if="shown" style="padding-left: 9px;"> <div v-if="shown" style="padding-left: 9px;">
<MkEmojiPickerSection <MkEmojiPickerSection
v-for="child in customEmojiTree" v-for="child in customEmojiTree"
:key="`custom:${child.value}`" :key="`custom:${child.value}`"
:initialShown="initialShown" :initialShown="initialShown"
:emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))" :emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))"
:hasChildSection="child.children.length !== 0" :hasChildSection="child.children.length !== 0"
:customEmojiTree="child.children" :customEmojiTree="child.children"
@chosen="nestedChosen" @chosen="nestedChosen"
> >
{{ child.value || i18n.ts.other }} {{ child.value || i18n.ts.other }}
</MkEmojiPickerSection> </MkEmojiPickerSection>
</div> </div>
<div v-if="shown" class="body"> <div v-if="shown" class="body">
<button <button
v-for="emoji in emojis" v-for="emoji in emojis"
:key="emoji" :key="emoji"
:data-emoji="emoji" :data-emoji="emoji"
class="_button item" class="_button item"
@pointerenter="computeButtonTitle" @pointerenter="computeButtonTitle"
@click="emit('chosen', emoji, $event)" @click="emit('chosen', emoji, $event)"
> >
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/> <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/> <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
</button> </button>
</div> </div>
</section> </section>
</template> </template>

View File

@@ -77,8 +77,8 @@ SPDX-License-Identifier: AGPL-3.0-only
:key="`custom:${child.value}`" :key="`custom:${child.value}`"
:initialShown="false" :initialShown="false"
:emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value).filter(filterAvailable).map(e => `:${e.name}:`))" :emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value).filter(filterAvailable).map(e => `:${e.name}:`))"
:hasChildSection="child.children.length !== 0" :hasChildSection="child.children.length !== 0"
:customEmojiTree="child.children" :customEmojiTree="child.children"
@chosen="chosen" @chosen="chosen"
> >
{{ child.value || i18n.ts.other }} {{ child.value || i18n.ts.other }}
@@ -103,12 +103,12 @@ import { ref, shallowRef, computed, watch, onMounted } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XSection from '@/components/MkEmojiPicker.section.vue'; import XSection from '@/components/MkEmojiPicker.section.vue';
import { import {
emojilist, emojilist,
emojiCharByCategory, emojiCharByCategory,
UnicodeEmojiDef, UnicodeEmojiDef,
unicodeEmojiCategories as categories, unicodeEmojiCategories as categories,
getEmojiName, getEmojiName,
CustomEmojiFolderTree CustomEmojiFolderTree,
} from '@/scripts/emojilist.js'; } from '@/scripts/emojilist.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
@@ -176,9 +176,9 @@ function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): Cu
} }
customEmojiCategories.value.forEach(ec => { customEmojiCategories.value.forEach(ec => {
if (ec !== null) { if (ec !== null) {
parseAndMergeCategories(ec, customEmojiFolderRoot); parseAndMergeCategories(ec, customEmojiFolderRoot);
} }
}); });
parseAndMergeCategories('', customEmojiFolderRoot); parseAndMergeCategories('', customEmojiFolderRoot);

View File

@@ -31,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue'; import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@@ -54,23 +55,23 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const modal = $shallowRef<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
const picker = $shallowRef<InstanceType<typeof MkEmojiPicker>>(); const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>();
function chosen(emoji: any) { function chosen(emoji: any) {
emit('done', emoji); emit('done', emoji);
if (props.choseAndClose) { if (props.choseAndClose) {
modal?.close(); modal.value?.close();
} }
} }
function opening() { function opening() {
picker?.reset(); picker.value?.reset();
picker?.focus(); picker.value?.focus();
// 何故かちょっと待たないとフォーカスされない // 何故かちょっと待たないとフォーカスされない
setTimeout(() => { setTimeout(() => {
picker?.focus(); picker.value?.focus();
}, 10); }, 10);
} }
</script> </script>

View File

@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { shallowRef, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
@@ -42,12 +42,12 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>(); const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
let caption = $ref(props.default); const caption = ref(props.default);
async function ok() { async function ok() {
emit('done', caption); emit('done', caption.value);
dialog.close(); dialog.value.close();
} }
</script> </script>

View File

@@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted } from 'vue'; import { nextTick, onMounted, shallowRef, ref } from 'vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@@ -70,10 +70,10 @@ const getBgColor = (el: HTMLElement) => {
} }
}; };
let rootEl = $shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
let bgSame = $ref(false); const bgSame = ref(false);
let opened = $ref(props.defaultOpen); const opened = ref(props.defaultOpen);
let openedAtLeastOnce = $ref(props.defaultOpen); const openedAtLeastOnce = ref(props.defaultOpen);
function enter(el) { function enter(el) {
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
@@ -98,20 +98,20 @@ function afterLeave(el) {
} }
function toggle() { function toggle() {
if (!opened) { if (!opened.value) {
openedAtLeastOnce = true; openedAtLeastOnce.value = true;
} }
nextTick(() => { nextTick(() => {
opened = !opened; opened.value = !opened.value;
}); });
} }
onMounted(() => { onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement); const computedStyle = getComputedStyle(document.documentElement);
const parentBg = getBgColor(rootEl.parentElement); const parentBg = getBgColor(rootEl.value.parentElement);
const myBg = computedStyle.getPropertyValue('--panel'); const myBg = computedStyle.getPropertyValue('--panel');
bgSame = parentBg === myBg; bgSame.value = parentBg === myBg;
}); });
</script> </script>

View File

@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onBeforeUnmount, onMounted } from 'vue'; import { onBeforeUnmount, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
@@ -57,9 +57,9 @@ const emit = defineEmits<{
(_: 'update:user', value: Misskey.entities.UserDetailed): void (_: 'update:user', value: Misskey.entities.UserDetailed): void
}>(); }>();
let isFollowing = $ref(props.user.isFollowing); const isFollowing = ref(props.user.isFollowing);
let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou); const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou);
let wait = $ref(false); const wait = ref(false);
const connection = useStream().useChannel('main'); const connection = useStream().useChannel('main');
if (props.user.isFollowing == null) { if (props.user.isFollowing == null) {
@@ -71,16 +71,16 @@ if (props.user.isFollowing == null) {
function onFollowChange(user: Misskey.entities.UserDetailed) { function onFollowChange(user: Misskey.entities.UserDetailed) {
if (user.id === props.user.id) { if (user.id === props.user.id) {
isFollowing = user.isFollowing; isFollowing.value = user.isFollowing;
hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
} }
} }
async function onClick() { async function onClick() {
wait = true; wait.value = true;
try { try {
if (isFollowing) { if (isFollowing.value) {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }), text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
@@ -92,11 +92,11 @@ async function onClick() {
userId: props.user.id, userId: props.user.id,
}); });
} else { } else {
if (hasPendingFollowRequestFromYou) { if (hasPendingFollowRequestFromYou.value) {
await os.api('following/requests/cancel', { await os.api('following/requests/cancel', {
userId: props.user.id, userId: props.user.id,
}); });
hasPendingFollowRequestFromYou = false; hasPendingFollowRequestFromYou.value = false;
} else { } else {
await os.api('following/create', { await os.api('following/create', {
userId: props.user.id, userId: props.user.id,
@@ -104,9 +104,9 @@ async function onClick() {
}); });
emit('update:user', { emit('update:user', {
...props.user, ...props.user,
withReplies: defaultStore.state.defaultWithReplies withReplies: defaultStore.state.defaultWithReplies,
}); });
hasPendingFollowRequestFromYou = true; hasPendingFollowRequestFromYou.value = true;
claimAchievement('following1'); claimAchievement('following1');
@@ -127,7 +127,7 @@ async function onClick() {
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} finally { } finally {
wait = false; wait.value = false;
} }
} }

View File

@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { ref } from 'vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
@@ -53,19 +53,19 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
let dialog: InstanceType<typeof MkModalWindow> = $ref(); const dialog = ref<InstanceType<typeof MkModalWindow>>();
let username = $ref(''); const username = ref('');
let email = $ref(''); const email = ref('');
let processing = $ref(false); const processing = ref(false);
async function onSubmit() { async function onSubmit() {
processing = true; processing.value = true;
await os.apiWithDialog('request-reset-password', { await os.apiWithDialog('request-reset-password', {
username, username: username.value,
email, email: email.value,
}); });
emit('done'); emit('done');
dialog.close(); dialog.value.close();
} }
</script> </script>

View File

@@ -26,11 +26,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template> <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</MkInput> </MkInput>
<MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text"> <MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" :mfmAutocomplete="form[item].treatAsMfm">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template> <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</MkInput> </MkInput>
<MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]"> <MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" :mfmAutocomplete="form[item].treatAsMfm" :mfmPreview="form[item].treatAsMfm">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template> <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</MkTextarea> </MkTextarea>

View File

@@ -23,7 +23,7 @@ const query = ref(props.q);
const search = () => { const search = () => {
const sp = new URLSearchParams(); const sp = new URLSearchParams();
sp.append('q', query.value); sp.append('q', query.value);
window.open(`https://www.google.com/search?${sp.toString()}`, '_blank'); window.open(`https://www.google.com/search?${sp.toString()}`, '_blank', 'noopener');
}; };
</script> </script>

View File

@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, nextTick, watch } from 'vue'; import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@@ -27,11 +27,11 @@ const props = defineProps<{
src: string; src: string;
}>(); }>();
const rootEl = $shallowRef<HTMLDivElement>(null); const rootEl = shallowRef<HTMLDivElement>(null);
const chartEl = $shallowRef<HTMLCanvasElement>(null); const chartEl = shallowRef<HTMLCanvasElement>(null);
const now = new Date(); const now = new Date();
let chartInstance: Chart = null; let chartInstance: Chart = null;
let fetching = $ref(true); const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({ const { handler: externalTooltipHandler } = useChartTooltip({
position: 'middle', position: 'middle',
@@ -42,8 +42,8 @@ async function renderChart() {
chartInstance.destroy(); chartInstance.destroy();
} }
const wide = rootEl.offsetWidth > 700; const wide = rootEl.value.offsetWidth > 700;
const narrow = rootEl.offsetWidth < 400; const narrow = rootEl.value.offsetWidth < 400;
const weeks = wide ? 50 : narrow ? 10 : 25; const weeks = wide ? 50 : narrow ? 10 : 25;
const chartLimit = 7 * weeks; const chartLimit = 7 * weeks;
@@ -88,7 +88,7 @@ async function renderChart() {
values = raw.deliverFailed; values = raw.deliverFailed;
} }
fetching = false; fetching.value = false;
await nextTick(); await nextTick();
@@ -101,7 +101,7 @@ async function renderChart() {
const marginEachCell = 4; const marginEachCell = 4;
chartInstance = new Chart(chartEl, { chartInstance = new Chart(chartEl.value, {
type: 'matrix', type: 'matrix',
data: { data: {
datasets: [{ datasets: [{
@@ -210,7 +210,7 @@ async function renderChart() {
} }
watch(() => props.src, () => { watch(() => props.src, () => {
fetching = true; fetching.value = true;
renderChart(); renderChart();
}); });

View File

@@ -21,7 +21,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts"> <script lang="ts">
import { $ref } from 'vue/macros';
import DrawBlurhash from '@/workers/draw-blurhash?worker'; import DrawBlurhash from '@/workers/draw-blurhash?worker';
import TestWebGL2 from '@/workers/test-webgl2?worker'; import TestWebGL2 from '@/workers/test-webgl2?worker';
import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js'; import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js';
@@ -58,7 +57,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue'; import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch, ref } from 'vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { render } from 'buraha'; import { render } from 'buraha';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@@ -98,41 +97,41 @@ const viewId = uuid();
const canvas = shallowRef<HTMLCanvasElement>(); const canvas = shallowRef<HTMLCanvasElement>();
const root = shallowRef<HTMLDivElement>(); const root = shallowRef<HTMLDivElement>();
const img = shallowRef<HTMLImageElement>(); const img = shallowRef<HTMLImageElement>();
let loaded = $ref(false); const loaded = ref(false);
let canvasWidth = $ref(64); const canvasWidth = ref(64);
let canvasHeight = $ref(64); const canvasHeight = ref(64);
let imgWidth = $ref(props.width); const imgWidth = ref(props.width);
let imgHeight = $ref(props.height); const imgHeight = ref(props.height);
let bitmapTmp = $ref<CanvasImageSource | undefined>(); const bitmapTmp = ref<CanvasImageSource | undefined>();
const hide = computed(() => !loaded || props.forceBlurhash); const hide = computed(() => !loaded.value || props.forceBlurhash);
function waitForDecode() { function waitForDecode() {
if (props.src != null && props.src !== '') { if (props.src != null && props.src !== '') {
nextTick() nextTick()
.then(() => img.value?.decode()) .then(() => img.value?.decode())
.then(() => { .then(() => {
loaded = true; loaded.value = true;
}, error => { }, error => {
console.log('Error occurred during decoding image', img.value, error); console.log('Error occurred during decoding image', img.value, error);
}); });
} else { } else {
loaded = false; loaded.value = false;
} }
} }
watch([() => props.width, () => props.height, root], () => { watch([() => props.width, () => props.height, root], () => {
const ratio = props.width / props.height; const ratio = props.width / props.height;
if (ratio > 1) { if (ratio > 1) {
canvasWidth = Math.round(64 * ratio); canvasWidth.value = Math.round(64 * ratio);
canvasHeight = 64; canvasHeight.value = 64;
} else { } else {
canvasWidth = 64; canvasWidth.value = 64;
canvasHeight = Math.round(64 / ratio); canvasHeight.value = Math.round(64 / ratio);
} }
const clientWidth = root.value?.clientWidth ?? 300; const clientWidth = root.value?.clientWidth ?? 300;
imgWidth = clientWidth; imgWidth.value = clientWidth;
imgHeight = Math.round(clientWidth / ratio); imgHeight.value = Math.round(clientWidth / ratio);
}, { }, {
immediate: true, immediate: true,
}); });
@@ -140,15 +139,15 @@ watch([() => props.width, () => props.height, root], () => {
function drawImage(bitmap: CanvasImageSource) { function drawImage(bitmap: CanvasImageSource) {
// canvasがないmountedされていない場合はTmpに保存しておく // canvasがないmountedされていない場合はTmpに保存しておく
if (!canvas.value) { if (!canvas.value) {
bitmapTmp = bitmap; bitmapTmp.value = bitmap;
return; return;
} }
// canvasがあれば描画する // canvasがあれば描画する
bitmapTmp = undefined; bitmapTmp.value = undefined;
const ctx = canvas.value.getContext('2d'); const ctx = canvas.value.getContext('2d');
if (!ctx) return; if (!ctx) return;
ctx.drawImage(bitmap, 0, 0, canvasWidth, canvasHeight); ctx.drawImage(bitmap, 0, 0, canvasWidth.value, canvasHeight.value);
} }
function drawAvg() { function drawAvg() {
@@ -160,7 +159,7 @@ function drawAvg() {
// avgColorでお茶をにごす // avgColorでお茶をにごす
ctx.beginPath(); ctx.beginPath();
ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888'; ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888';
ctx.fillRect(0, 0, canvasWidth, canvasHeight); ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
} }
async function draw() { async function draw() {
@@ -212,8 +211,8 @@ watch(() => props.hash, () => {
onMounted(() => { onMounted(() => {
// drawImageがmountedより先に呼ばれている場合はここで描画する // drawImageがmountedより先に呼ばれている場合はここで描画する
if (bitmapTmp) { if (bitmapTmp.value) {
drawImage(bitmapTmp); drawImage(bitmapTmp.value);
} }
waitForDecode(); waitForDecode();
}); });

View File

@@ -43,11 +43,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
import { debounce } from 'throttle-debounce'; import { debounce } from 'throttle-debounce';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { useInterval } from '@/scripts/use-interval.js'; import { useInterval } from '@/scripts/use-interval.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
const props = defineProps<{ const props = defineProps<{
modelValue: string | number | null; modelValue: string | number | null;
@@ -59,6 +60,7 @@ const props = defineProps<{
placeholder?: string; placeholder?: string;
autofocus?: boolean; autofocus?: boolean;
autocomplete?: string; autocomplete?: string;
mfmAutocomplete?: boolean | SuggestionType[],
autocapitalize?: string; autocapitalize?: string;
spellcheck?: boolean; spellcheck?: boolean;
step?: any; step?: any;
@@ -93,6 +95,7 @@ const height =
props.small ? 33 : props.small ? 33 :
props.large ? 39 : props.large ? 39 :
36; 36;
let autocomplete: Autocomplete;
const focus = () => inputEl.value.focus(); const focus = () => inputEl.value.focus();
const onInput = (ev: KeyboardEvent) => { const onInput = (ev: KeyboardEvent) => {
@@ -160,6 +163,16 @@ onMounted(() => {
focus(); focus();
} }
}); });
if (props.mfmAutocomplete) {
autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete);
}
});
onUnmounted(() => {
if (autocomplete) {
autocomplete.detach();
}
}); });
defineExpose({ defineExpose({

View File

@@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkMiniChart from '@/components/MkMiniChart.vue'; import MkMiniChart from '@/components/MkMiniChart.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
@@ -24,12 +25,12 @@ const props = defineProps<{
instance: Misskey.entities.FederationInstance; instance: Misskey.entities.FederationInstance;
}>(); }>();
let chartValues = $ref<number[] | null>(null); const chartValues = ref<number[] | null>(null);
os.apiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => { os.apiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => {
// 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く // 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く
res['requests.received'].splice(0, 1); res['requests.received'].splice(0, 1);
chartValues = res['requests.received']; chartValues.value = res['requests.received'];
}); });
function getInstanceIcon(instance): string { function getInstanceIcon(instance): string {

View File

@@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted, ref, shallowRef } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkChart from '@/components/MkChart.vue'; import MkChart from '@/components/MkChart.vue';
@@ -100,11 +100,11 @@ import { initChart } from '@/scripts/init-chart.js';
initChart(); initChart();
const chartLimit = 500; const chartLimit = 500;
let chartSpan = $ref<'hour' | 'day'>('hour'); const chartSpan = ref<'hour' | 'day'>('hour');
let chartSrc = $ref('active-users'); const chartSrc = ref('active-users');
let heatmapSrc = $ref('active-users'); const heatmapSrc = ref('active-users');
let subDoughnutEl = $shallowRef<HTMLCanvasElement>(); const subDoughnutEl = shallowRef<HTMLCanvasElement>();
let pubDoughnutEl = $shallowRef<HTMLCanvasElement>(); const pubDoughnutEl = shallowRef<HTMLCanvasElement>();
const { handler: externalTooltipHandler1 } = useChartTooltip({ const { handler: externalTooltipHandler1 } = useChartTooltip({
position: 'middle', position: 'middle',
@@ -163,7 +163,7 @@ function createDoughnut(chartEl, tooltip, data) {
onMounted(() => { onMounted(() => {
os.apiGet('federation/stats', { limit: 30 }).then(fedStats => { os.apiGet('federation/stats', { limit: 30 }).then(fedStats => {
createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({ createDoughnut(subDoughnutEl.value, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
name: x.host, name: x.host,
color: x.themeColor, color: x.themeColor,
value: x.followersCount, value: x.followersCount,
@@ -172,7 +172,7 @@ onMounted(() => {
}, },
})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }])); })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]));
createDoughnut(pubDoughnutEl, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({ createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({
name: x.host, name: x.host,
color: x.themeColor, color: x.themeColor,
value: x.followingCount, value: x.followingCount,

View File

@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { computed } from 'vue';
import { instanceName } from '@/config.js'; import { instanceName } from '@/config.js';
import { instance as Instance } from '@/instance.js'; import { instance as Instance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
@@ -30,7 +30,7 @@ const instance = props.instance ?? {
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content, themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
}; };
const faviconUrl = $computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico'); const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
const themeColor = instance.themeColor ?? '#777777'; const themeColor = instance.themeColor ?? '#777777';

View File

@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import { navbarItemDef } from '@/navbar.js'; import { navbarItemDef } from '@/navbar.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@@ -48,7 +48,7 @@ const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'pop
deviceKind === 'smartphone' ? 'drawer' : deviceKind === 'smartphone' ? 'drawer' :
'dialog'; 'dialog';
const modal = $shallowRef<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
const menu = defaultStore.state.menu; const menu = defaultStore.state.menu;
@@ -63,7 +63,7 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k =>
})); }));
function close() { function close() {
modal.close(); modal.value.close();
} }
</script> </script>

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<component <component
:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel" :target="target" :is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel ?? 'nofollow noopener'" :target="target"
:title="url" :title="url"
> >
<slot></slot> <slot></slot>
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent, ref } from 'vue';
import { url as local } from '@/config.js'; import { url as local } from '@/config.js';
import { useTooltip } from '@/scripts/use-tooltip.js'; import { useTooltip } from '@/scripts/use-tooltip.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
@@ -29,13 +29,13 @@ const self = props.url.startsWith(local);
const attr = self ? 'to' : 'href'; const attr = self ? 'to' : 'href';
const target = self ? null : '_blank'; const target = self ? null : '_blank';
const el = $ref(); const el = ref();
useTooltip($$(el), (showing) => { useTooltip(el, (showing) => {
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
showing, showing,
url: props.url, url: props.url,
source: el, source: el.value,
}, {}, 'closed'); }, {}, 'closed');
}); });
</script> </script>

View File

@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, shallowRef, watch } from 'vue'; import { onMounted, shallowRef, watch, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@@ -42,7 +42,7 @@ const props = withDefaults(defineProps<{
}); });
const audioEl = shallowRef<HTMLAudioElement>(); const audioEl = shallowRef<HTMLAudioElement>();
let hide = $ref(true); const hide = ref(true);
watch(audioEl, () => { watch(audioEl, () => {
if (audioEl.value) { if (audioEl.value) {

View File

@@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch } from 'vue'; import { watch, ref, computed } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import bytes from '@/filters/bytes.js'; import bytes from '@/filters/bytes.js';
@@ -73,10 +73,10 @@ const props = withDefaults(defineProps<{
controls: true, controls: true,
}); });
let hide = $ref(true); const hide = ref(true);
let darkMode: boolean = $ref(defaultStore.state.darkMode); const darkMode = ref<boolean>(defaultStore.state.darkMode);
const url = $computed(() => (props.raw || defaultStore.state.loadRawImages) const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
? props.image.url ? props.image.url
: defaultStore.state.disableShowingAnimatedImages : defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(props.image.url) ? getStaticImageUrl(props.image.url)
@@ -87,14 +87,14 @@ function onclick() {
if (!props.controls) { if (!props.controls) {
return; return;
} }
if (hide) { if (hide.value) {
hide = false; hide.value = false;
} }
} }
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする // Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
watch(() => props.image, () => { watch(() => props.image, () => {
hide = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore'); hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
}, { }, {
deep: true, deep: true,
immediate: true, immediate: true,
@@ -105,7 +105,7 @@ function showMenu(ev: MouseEvent) {
text: i18n.ts.hide, text: i18n.ts.hide,
icon: 'ti ti-eye-off', icon: 'ti ti-eye-off',
action: () => { action: () => {
hide = true; hide.value = true;
}, },
}, ...(iAmModerator ? [{ }, ...(iAmModerator ? [{
text: i18n.ts.markAsSensitive, text: i18n.ts.markAsSensitive,
@@ -126,7 +126,7 @@ function showMenu(ev: MouseEvent) {
.sensitive { .sensitive {
position: relative; position: relative;
&::after { &::after {
content: ""; content: "";
position: absolute; position: absolute;

View File

@@ -63,7 +63,7 @@ async function getClientWidthWithCache(targetEl: HTMLElement, containerEl: HTMLE
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, shallowRef } from 'vue'; import { computed, onMounted, onUnmounted, shallowRef } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import PhotoSwipeLightbox from 'photoswipe/lightbox'; import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe'; import PhotoSwipe from 'photoswipe';
@@ -86,7 +86,7 @@ const container = shallowRef<HTMLElement | null | undefined>(undefined);
const gallery = shallowRef<HTMLDivElement>(); const gallery = shallowRef<HTMLDivElement>();
const pswpZIndex = os.claimZIndex('middle'); const pswpZIndex = os.claimZIndex('middle');
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString()); document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
const count = $computed(() => props.mediaList.filter(media => previewable(media)).length); const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
let lightbox: PhotoSwipeLightbox | null; let lightbox: PhotoSwipeLightbox | null;
const popstateHandler = (): void => { const popstateHandler = (): void => {

View File

@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@contextmenu.self="e => e.preventDefault()" @contextmenu.self="e => e.preventDefault()"
> >
<template v-for="(item, i) in items2"> <template v-for="(item, i) in items2">
<div v-if="item === null" role="separator" :class="$style.divider"></div> <div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]"> <span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
<span>{{ item.text }}</span> <span>{{ item.text }}</span>
</span> </span>
@@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts"> <script lang="ts">
import { Ref, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { Ref, computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus.js'; import { focusPrev, focusNext } from '@/scripts/focus.js';
import MkSwitchButton from '@/components/MkSwitch.button.vue'; import MkSwitchButton from '@/components/MkSwitch.button.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu'; import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu';
@@ -90,19 +90,19 @@ const emit = defineEmits<{
(ev: 'hide'): void; (ev: 'hide'): void;
}>(); }>();
let itemsEl = $shallowRef<HTMLDivElement>(); const itemsEl = shallowRef<HTMLDivElement>();
let items2: InnerMenuItem[] = $ref([]); const items2 = ref<InnerMenuItem[]>([]);
let child = $shallowRef<InstanceType<typeof XChild>>(); const child = shallowRef<InstanceType<typeof XChild>>();
let keymap = $computed(() => ({ const keymap = computed(() => ({
'up|k|shift+tab': focusUp, 'up|k|shift+tab': focusUp,
'down|j|tab': focusDown, 'down|j|tab': focusDown,
'esc': close, 'esc': close,
})); }));
let childShowingItem = $ref<MenuItem | null>(); const childShowingItem = ref<MenuItem | null>();
let preferClick = isTouchUsing || props.asDrawer; let preferClick = isTouchUsing || props.asDrawer;
@@ -115,22 +115,22 @@ watch(() => props.items, () => {
if (item && 'then' in item) { // if item is Promise if (item && 'then' in item) { // if item is Promise
items[i] = { type: 'pending' }; items[i] = { type: 'pending' };
item.then(actualItem => { item.then(actualItem => {
items2[i] = actualItem; items2.value[i] = actualItem;
}); });
} }
} }
items2 = items as InnerMenuItem[]; items2.value = items as InnerMenuItem[];
}, { }, {
immediate: true, immediate: true,
}); });
const childMenu = ref<MenuItem[] | null>(); const childMenu = ref<MenuItem[] | null>();
let childTarget = $shallowRef<HTMLElement | null>(); const childTarget = shallowRef<HTMLElement | null>();
function closeChild() { function closeChild() {
childMenu.value = null; childMenu.value = null;
childShowingItem = null; childShowingItem.value = null;
} }
function childActioned() { function childActioned() {
@@ -139,8 +139,8 @@ function childActioned() {
} }
const onGlobalMousedown = (event: MouseEvent) => { const onGlobalMousedown = (event: MouseEvent) => {
if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return; if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target))) return;
if (child && child.checkHit(event)) return; if (child.value && child.value.checkHit(event)) return;
closeChild(); closeChild();
}; };
@@ -177,10 +177,10 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
}); });
emit('hide'); emit('hide');
} else { } else {
childTarget = ev.currentTarget ?? ev.target; childTarget.value = ev.currentTarget ?? ev.target;
// これでもリアクティビティは保たれる // これでもリアクティビティは保たれる
childMenu.value = children; childMenu.value = children;
childShowingItem = item; childShowingItem.value = item;
} }
} }
@@ -202,14 +202,14 @@ function focusDown() {
} }
function switchItem(item: MenuSwitch & { ref: any }) { function switchItem(item: MenuSwitch & { ref: any }) {
if (typeof item.disabled === 'boolean' ? item.disabled : item.disabled.value) return; if (item.disabled !== undefined && (typeof item.disabled === 'boolean' ? item.disabled : item.disabled.value)) return;
item.ref = !item.ref; item.ref = !item.ref;
} }
onMounted(() => { onMounted(() => {
if (props.viaKeyboard) { if (props.viaKeyboard) {
nextTick(() => { nextTick(() => {
if (itemsEl) focusNext(itemsEl.children[0], true, false); if (itemsEl.value) focusNext(itemsEl.value.children[0], true, false);
}); });
} }

View File

@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch } from 'vue'; import { watch, ref } from 'vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { useInterval } from '@/scripts/use-interval.js'; import { useInterval } from '@/scripts/use-interval.js';
@@ -43,11 +43,11 @@ const props = defineProps<{
const viewBoxX = 50; const viewBoxX = 50;
const viewBoxY = 50; const viewBoxY = 50;
const gradientId = uuid(); const gradientId = uuid();
let polylinePoints = $ref(''); const polylinePoints = ref('');
let polygonPoints = $ref(''); const polygonPoints = ref('');
let headX = $ref<number | null>(null); const headX = ref<number | null>(null);
let headY = $ref<number | null>(null); const headY = ref<number | null>(null);
let clock = $ref<number | null>(null); const clock = ref<number | null>(null);
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent')); const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
const color = accent.toRgbString(); const color = accent.toRgbString();
@@ -60,12 +60,12 @@ function draw(): void {
(1 - (n / peak)) * viewBoxY, (1 - (n / peak)) * viewBoxY,
]); ]);
polylinePoints = _polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); polylinePoints.value = _polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
polygonPoints = `0,${ viewBoxY } ${ polylinePoints } ${ viewBoxX },${ viewBoxY }`; polygonPoints.value = `0,${ viewBoxY } ${ polylinePoints.value } ${ viewBoxX },${ viewBoxY }`;
headX = _polylinePoints.at(-1)![0]; headX.value = _polylinePoints.at(-1)![0];
headY = _polylinePoints.at(-1)![1]; headY.value = _polylinePoints.at(-1)![1];
} }
watch(() => props.src, draw, { immediate: true }); watch(() => props.src, draw, { immediate: true });

View File

@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch } from 'vue'; import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch, ref, shallowRef, computed } from 'vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { isTouchUsing } from '@/scripts/touch.js'; import { isTouchUsing } from '@/scripts/touch.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@@ -89,14 +89,14 @@ const emit = defineEmits<{
provide('modal', true); provide('modal', true);
let maxHeight = $ref<number>(); const maxHeight = ref<number>();
let fixed = $ref(false); const fixed = ref(false);
let transformOrigin = $ref('center'); const transformOrigin = ref('center');
let showing = $ref(true); const showing = ref(true);
let content = $shallowRef<HTMLElement>(); const content = shallowRef<HTMLElement>();
const zIndex = os.claimZIndex(props.zPriority); const zIndex = os.claimZIndex(props.zPriority);
let useSendAnime = $ref(false); const useSendAnime = ref(false);
const type = $computed<ModalTypes>(() => { const type = computed<ModalTypes>(() => {
if (props.preferType === 'auto') { if (props.preferType === 'auto') {
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') { if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
return 'drawer'; return 'drawer';
@@ -107,26 +107,26 @@ const type = $computed<ModalTypes>(() => {
return props.preferType!; return props.preferType!;
} }
}); });
const isEnableBgTransparent = $computed(() => props.transparentBg && (type === 'popup')); const isEnableBgTransparent = computed(() => props.transparentBg && (type.value === 'popup'));
let transitionName = $computed((() => const transitionName = computed((() =>
defaultStore.state.animation defaultStore.state.animation
? useSendAnime ? useSendAnime.value
? 'send' ? 'send'
: type === 'drawer' : type.value === 'drawer'
? 'modal-drawer' ? 'modal-drawer'
: type === 'popup' : type.value === 'popup'
? 'modal-popup' ? 'modal-popup'
: 'modal' : 'modal'
: '' : ''
)); ));
let transitionDuration = $computed((() => const transitionDuration = computed((() =>
transitionName === 'send' transitionName.value === 'send'
? 400 ? 400
: transitionName === 'modal-popup' : transitionName.value === 'modal-popup'
? 100 ? 100
: transitionName === 'modal' : transitionName.value === 'modal'
? 200 ? 200
: transitionName === 'modal-drawer' : transitionName.value === 'modal-drawer'
? 200 ? 200
: 0 : 0
)); ));
@@ -135,12 +135,12 @@ let contentClicking = false;
function close(opts: { useSendAnimation?: boolean } = {}) { function close(opts: { useSendAnimation?: boolean } = {}) {
if (opts.useSendAnimation) { if (opts.useSendAnimation) {
useSendAnime = true; useSendAnime.value = true;
} }
// eslint-disable-next-line vue/no-mutating-props // eslint-disable-next-line vue/no-mutating-props
if (props.src) props.src.style.pointerEvents = 'auto'; if (props.src) props.src.style.pointerEvents = 'auto';
showing = false; showing.value = false;
emit('close'); emit('close');
} }
@@ -149,8 +149,8 @@ function onBgClick() {
emit('click'); emit('click');
} }
if (type === 'drawer') { if (type.value === 'drawer') {
maxHeight = window.innerHeight / 1.5; maxHeight.value = window.innerHeight / 1.5;
} }
const keymap = { const keymap = {
@@ -162,21 +162,21 @@ const SCROLLBAR_THICKNESS = 16;
const align = () => { const align = () => {
if (props.src == null) return; if (props.src == null) return;
if (type === 'drawer') return; if (type.value === 'drawer') return;
if (type === 'dialog') return; if (type.value === 'dialog') return;
if (content == null) return; if (content.value == null) return;
const srcRect = props.src.getBoundingClientRect(); const srcRect = props.src.getBoundingClientRect();
const width = content!.offsetWidth; const width = content.value!.offsetWidth;
const height = content!.offsetHeight; const height = content.value!.offsetHeight;
let left; let left;
let top; let top;
const x = srcRect.left + (fixed ? 0 : window.pageXOffset); const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset);
const y = srcRect.top + (fixed ? 0 : window.pageYOffset); const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset);
if (props.anchor.x === 'center') { if (props.anchor.x === 'center') {
left = x + (props.src.offsetWidth / 2) - (width / 2); left = x + (props.src.offsetWidth / 2) - (width / 2);
@@ -194,7 +194,7 @@ const align = () => {
top = y + props.src.offsetHeight; top = y + props.src.offsetHeight;
} }
if (fixed) { if (fixed.value) {
// 画面から横にはみ出る場合 // 画面から横にはみ出る場合
if (left + width > (window.innerWidth - SCROLLBAR_THICKNESS)) { if (left + width > (window.innerWidth - SCROLLBAR_THICKNESS)) {
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width; left = (window.innerWidth - SCROLLBAR_THICKNESS) - width;
@@ -207,16 +207,16 @@ const align = () => {
if (top + height > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) { if (top + height > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
if (props.noOverlap && props.anchor.x === 'center') { if (props.noOverlap && props.anchor.x === 'center') {
if (underSpace >= (upperSpace / 3)) { if (underSpace >= (upperSpace / 3)) {
maxHeight = underSpace; maxHeight.value = underSpace;
} else { } else {
maxHeight = upperSpace; maxHeight.value = upperSpace;
top = (upperSpace + MARGIN) - height; top = (upperSpace + MARGIN) - height;
} }
} else { } else {
top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height; top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height;
} }
} else { } else {
maxHeight = underSpace; maxHeight.value = underSpace;
} }
} else { } else {
// 画面から横にはみ出る場合 // 画面から横にはみ出る場合
@@ -231,16 +231,16 @@ const align = () => {
if (top + height - window.pageYOffset > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) { if (top + height - window.pageYOffset > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
if (props.noOverlap && props.anchor.x === 'center') { if (props.noOverlap && props.anchor.x === 'center') {
if (underSpace >= (upperSpace / 3)) { if (underSpace >= (upperSpace / 3)) {
maxHeight = underSpace; maxHeight.value = underSpace;
} else { } else {
maxHeight = upperSpace; maxHeight.value = upperSpace;
top = window.pageYOffset + ((upperSpace + MARGIN) - height); top = window.pageYOffset + ((upperSpace + MARGIN) - height);
} }
} else { } else {
top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.pageYOffset - 1; top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.pageYOffset - 1;
} }
} else { } else {
maxHeight = underSpace; maxHeight.value = underSpace;
} }
} }
@@ -255,29 +255,29 @@ const align = () => {
let transformOriginX = 'center'; let transformOriginX = 'center';
let transformOriginY = 'center'; let transformOriginY = 'center';
if (top >= srcRect.top + props.src.offsetHeight + (fixed ? 0 : window.pageYOffset)) { if (top >= srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.pageYOffset)) {
transformOriginY = 'top'; transformOriginY = 'top';
} else if ((top + height) <= srcRect.top + (fixed ? 0 : window.pageYOffset)) { } else if ((top + height) <= srcRect.top + (fixed.value ? 0 : window.pageYOffset)) {
transformOriginY = 'bottom'; transformOriginY = 'bottom';
} }
if (left >= srcRect.left + props.src.offsetWidth + (fixed ? 0 : window.pageXOffset)) { if (left >= srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.pageXOffset)) {
transformOriginX = 'left'; transformOriginX = 'left';
} else if ((left + width) <= srcRect.left + (fixed ? 0 : window.pageXOffset)) { } else if ((left + width) <= srcRect.left + (fixed.value ? 0 : window.pageXOffset)) {
transformOriginX = 'right'; transformOriginX = 'right';
} }
transformOrigin = `${transformOriginX} ${transformOriginY}`; transformOrigin.value = `${transformOriginX} ${transformOriginY}`;
content.style.left = left + 'px'; content.value.style.left = left + 'px';
content.style.top = top + 'px'; content.value.style.top = top + 'px';
}; };
const onOpened = () => { const onOpened = () => {
emit('opened'); emit('opened');
// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
const el = content!.children[0]; const el = content.value!.children[0];
el.addEventListener('mousedown', ev => { el.addEventListener('mousedown', ev => {
contentClicking = true; contentClicking = true;
window.addEventListener('mouseup', ev => { window.addEventListener('mouseup', ev => {
@@ -299,7 +299,7 @@ onMounted(() => {
// eslint-disable-next-line vue/no-mutating-props // eslint-disable-next-line vue/no-mutating-props
props.src.style.pointerEvents = 'none'; props.src.style.pointerEvents = 'none';
} }
fixed = (type === 'drawer') || (getFixedContainer(props.src) != null); fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
await nextTick(); await nextTick();
@@ -307,7 +307,7 @@ onMounted(() => {
}, { immediate: true }); }, { immediate: true });
nextTick(() => { nextTick(() => {
alignObserver.observe(content!); alignObserver.observe(content.value!);
}); });
}); });

View File

@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue'; import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
import MkModal from './MkModal.vue'; import MkModal from './MkModal.vue';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@@ -44,14 +44,14 @@ const emit = defineEmits<{
(event: 'ok'): void; (event: 'ok'): void;
}>(); }>();
let modal = $shallowRef<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
let rootEl = $shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
let headerEl = $shallowRef<HTMLElement>(); const headerEl = shallowRef<HTMLElement>();
let bodyWidth = $ref(0); const bodyWidth = ref(0);
let bodyHeight = $ref(0); const bodyHeight = ref(0);
const close = () => { const close = () => {
modal.close(); modal.value.close();
}; };
const onBgClick = () => { const onBgClick = () => {
@@ -67,14 +67,14 @@ const onKeydown = (evt) => {
}; };
const ro = new ResizeObserver((entries, observer) => { const ro = new ResizeObserver((entries, observer) => {
bodyWidth = rootEl.offsetWidth; bodyWidth.value = rootEl.value.offsetWidth;
bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight; bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
}); });
onMounted(() => { onMounted(() => {
bodyWidth = rootEl.offsetWidth; bodyWidth.value = rootEl.value.offsetWidth;
bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight; bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
ro.observe(rootEl); ro.observe(rootEl.value);
}); });
onUnmounted(() => { onUnmounted(() => {

View File

@@ -205,12 +205,12 @@ const emit = defineEmits<{
const inChannel = inject('inChannel', null); const inChannel = inject('inChannel', null);
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
let note = $ref(deepClone(props.note)); const note = ref(deepClone(props.note));
// plugin // plugin
if (noteViewInterruptors.length > 0) { if (noteViewInterruptors.length > 0) {
onMounted(async () => { onMounted(async () => {
let result: Misskey.entities.Note | null = deepClone(note); let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) { for (const interruptor of noteViewInterruptors) {
try { try {
result = await interruptor.handler(result); result = await interruptor.handler(result);
@@ -222,15 +222,15 @@ if (noteViewInterruptors.length > 0) {
console.error(err); console.error(err);
} }
} }
note = result; note.value = result;
}); });
} }
const isRenote = ( const isRenote = (
note.renote != null && note.value.renote != null &&
note.text == null && note.value.text == null &&
note.fileIds.length === 0 && note.value.fileIds.length === 0 &&
note.poll == null note.value.poll == null
); );
const el = shallowRef<HTMLElement>(); const el = shallowRef<HTMLElement>();
@@ -239,21 +239,21 @@ const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note); const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
const isMyRenote = $i && ($i.id === note.userId); const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(false); const showContent = ref(false);
const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null); const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
const urls = $computed(() => parsed ? extractUrlFromMfm(parsed) : null); const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value) : null);
const isLong = shouldCollapsed(appearNote, urls ?? []); const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
const collapsed = ref(appearNote.cw == null && isLong); const collapsed = ref(appearNote.value.cw == null && isLong);
const isDeleted = ref(false); const isDeleted = ref(false);
const muted = ref(checkMute(appearNote, $i?.mutedWords)); const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
const hardMuted = ref(props.withHardMute && checkMute(appearNote, $i?.hardMutedWords)); const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords));
const translation = ref<any>(null); const translation = ref<any>(null);
const translating = ref(false); const translating = ref(false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id)); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id));
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null))); const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null)));
function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean { function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
if (mutedWords == null) return false; if (mutedWords == null) return false;
@@ -277,20 +277,20 @@ const keymap = {
provide('react', (reaction: string) => { provide('react', (reaction: string) => {
os.api('notes/reactions/create', { os.api('notes/reactions/create', {
noteId: appearNote.id, noteId: appearNote.value.id,
reaction: reaction, reaction: reaction,
}); });
}); });
if (props.mock) { if (props.mock) {
watch(() => props.note, (to) => { watch(() => props.note, (to) => {
note = deepClone(to); note.value = deepClone(to);
}, { deep: true }); }, { deep: true });
} else { } else {
useNoteCapture({ useNoteCapture({
rootEl: el, rootEl: el,
note: $$(appearNote), note: appearNote,
pureNote: $$(note), pureNote: note,
isDeletedRef: isDeleted, isDeletedRef: isDeleted,
}); });
} }
@@ -298,7 +298,7 @@ if (props.mock) {
if (!props.mock) { if (!props.mock) {
useTooltip(renoteButton, async (showing) => { useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', { const renotes = await os.api('notes/renotes', {
noteId: appearNote.id, noteId: appearNote.value.id,
limit: 11, limit: 11,
}); });
@@ -309,7 +309,7 @@ if (!props.mock) {
os.popup(MkUsersTooltip, { os.popup(MkUsersTooltip, {
showing, showing,
users, users,
count: appearNote.renoteCount, count: appearNote.value.renoteCount,
targetElement: renoteButton.value, targetElement: renoteButton.value,
}, {}, 'closed'); }, {}, 'closed');
}); });
@@ -319,7 +319,7 @@ function renote(viaKeyboard = false) {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
const { menu } = getRenoteMenu({ note: note, renoteButton, mock: props.mock }); const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
os.popupMenu(menu, renoteButton.value, { os.popupMenu(menu, renoteButton.value, {
viaKeyboard, viaKeyboard,
}); });
@@ -331,8 +331,8 @@ function reply(viaKeyboard = false): void {
return; return;
} }
os.post({ os.post({
reply: appearNote, reply: appearNote.value,
channel: appearNote.channel, channel: appearNote.value.channel,
animation: !viaKeyboard, animation: !viaKeyboard,
}, () => { }, () => {
focus(); focus();
@@ -342,7 +342,7 @@ function reply(viaKeyboard = false): void {
function react(viaKeyboard = false): void { function react(viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
if (appearNote.reactionAcceptance === 'likeOnly') { if (appearNote.value.reactionAcceptance === 'likeOnly') {
sound.play('reaction'); sound.play('reaction');
if (props.mock) { if (props.mock) {
@@ -350,7 +350,7 @@ function react(viaKeyboard = false): void {
} }
os.api('notes/reactions/create', { os.api('notes/reactions/create', {
noteId: appearNote.id, noteId: appearNote.value.id,
reaction: '❤️', reaction: '❤️',
}); });
const el = reactButton.value as HTMLElement | null | undefined; const el = reactButton.value as HTMLElement | null | undefined;
@@ -371,10 +371,10 @@ function react(viaKeyboard = false): void {
} }
os.api('notes/reactions/create', { os.api('notes/reactions/create', {
noteId: appearNote.id, noteId: appearNote.value.id,
reaction: reaction, reaction: reaction,
}); });
if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead'); claimAchievement('reactWithoutRead');
} }
}, () => { }, () => {
@@ -417,7 +417,7 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault(); ev.preventDefault();
react(); react();
} else { } else {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.contextMenu(menu, ev).then(focus).finally(cleanup); os.contextMenu(menu, ev).then(focus).finally(cleanup);
} }
} }
@@ -427,7 +427,7 @@ function menu(viaKeyboard = false): void {
return; return;
} }
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.popupMenu(menu, menuButton.value, { os.popupMenu(menu, menuButton.value, {
viaKeyboard, viaKeyboard,
}).then(focus).finally(cleanup); }).then(focus).finally(cleanup);
@@ -438,7 +438,7 @@ async function clip() {
return; return;
} }
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
} }
function showRenoteMenu(viaKeyboard = false): void { function showRenoteMenu(viaKeyboard = false): void {
@@ -453,7 +453,7 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true, danger: true,
action: () => { action: () => {
os.api('notes/delete', { os.api('notes/delete', {
noteId: note.id, noteId: note.value.id,
}); });
isDeleted.value = true; isDeleted.value = true;
}, },
@@ -463,17 +463,17 @@ function showRenoteMenu(viaKeyboard = false): void {
if (isMyRenote) { if (isMyRenote) {
pleaseLogin(); pleaseLogin();
os.popupMenu([ os.popupMenu([
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote), getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
null, { type: 'divider' },
getUnrenote(), getUnrenote(),
], renoteTime.value, { ], renoteTime.value, {
viaKeyboard: viaKeyboard, viaKeyboard: viaKeyboard,
}); });
} else { } else {
os.popupMenu([ os.popupMenu([
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote), getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
null, { type: 'divider' },
getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote), getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
$i.isModerator || $i.isAdmin ? getUnrenote() : undefined, $i.isModerator || $i.isAdmin ? getUnrenote() : undefined,
], renoteTime.value, { ], renoteTime.value, {
viaKeyboard: viaKeyboard, viaKeyboard: viaKeyboard,
@@ -499,7 +499,7 @@ function focusAfter() {
function readPromo() { function readPromo() {
os.api('promo/read', { os.api('promo/read', {
noteId: appearNote.id, noteId: appearNote.value.id,
}); });
isDeleted.value = true; isDeleted.value = true;
} }

View File

@@ -145,7 +145,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button> <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button>
</div> </div>
<div> <div>
<div v-if="tab === 'replies'" :class="$style.tab_replies"> <div v-if="tab === 'replies'">
<div v-if="!repliesLoaded" style="padding: 16px"> <div v-if="!repliesLoaded" style="padding: 16px">
<MkButton style="margin: 0 auto;" primary rounded @click="loadReplies">{{ i18n.ts.loadReplies }}</MkButton> <MkButton style="margin: 0 auto;" primary rounded @click="loadReplies">{{ i18n.ts.loadReplies }}</MkButton>
</div> </div>
@@ -235,12 +235,12 @@ const props = defineProps<{
const inChannel = inject('inChannel', null); const inChannel = inject('inChannel', null);
let note = $ref(deepClone(props.note)); const note = ref(deepClone(props.note));
// plugin // plugin
if (noteViewInterruptors.length > 0) { if (noteViewInterruptors.length > 0) {
onMounted(async () => { onMounted(async () => {
let result: Misskey.entities.Note | null = deepClone(note); let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) { for (const interruptor of noteViewInterruptors) {
try { try {
result = await interruptor.handler(result); result = await interruptor.handler(result);
@@ -252,15 +252,15 @@ if (noteViewInterruptors.length > 0) {
console.error(err); console.error(err);
} }
} }
note = result; note.value = result;
}); });
} }
const isRenote = ( const isRenote = (
note.renote != null && note.value.renote != null &&
note.text == null && note.value.text == null &&
note.fileIds.length === 0 && note.value.fileIds.length === 0 &&
note.poll == null note.value.poll == null
); );
const el = shallowRef<HTMLElement>(); const el = shallowRef<HTMLElement>();
@@ -269,19 +269,19 @@ const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note); const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
const isMyRenote = $i && ($i.id === note.userId); const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(false); const showContent = ref(false);
const isDeleted = ref(false); const isDeleted = ref(false);
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false);
const translation = ref(null); const translation = ref(null);
const translating = ref(false); const translating = ref(false);
const parsed = appearNote.text ? mfm.parse(appearNote.text) : null; const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
const urls = parsed ? extractUrlFromMfm(parsed) : null; const urls = parsed ? extractUrlFromMfm(parsed) : null;
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
const conversation = ref<Misskey.entities.Note[]>([]); const conversation = ref<Misskey.entities.Note[]>([]);
const replies = ref<Misskey.entities.Note[]>([]); const replies = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i.id);
const keymap = { const keymap = {
'r': () => reply(true), 'r': () => reply(true),
@@ -294,41 +294,41 @@ const keymap = {
provide('react', (reaction: string) => { provide('react', (reaction: string) => {
os.api('notes/reactions/create', { os.api('notes/reactions/create', {
noteId: appearNote.id, noteId: appearNote.value.id,
reaction: reaction, reaction: reaction,
}); });
}); });
let tab = $ref('replies'); const tab = ref('replies');
let reactionTabType = $ref(null); const reactionTabType = ref(null);
const renotesPagination = $computed(() => ({ const renotesPagination = computed(() => ({
endpoint: 'notes/renotes', endpoint: 'notes/renotes',
limit: 10, limit: 10,
params: { params: {
noteId: appearNote.id, noteId: appearNote.value.id,
}, },
})); }));
const reactionsPagination = $computed(() => ({ const reactionsPagination = computed(() => ({
endpoint: 'notes/reactions', endpoint: 'notes/reactions',
limit: 10, limit: 10,
params: { params: {
noteId: appearNote.id, noteId: appearNote.value.id,
type: reactionTabType, type: reactionTabType.value,
}, },
})); }));
useNoteCapture({ useNoteCapture({
rootEl: el, rootEl: el,
note: $$(appearNote), note: appearNote,
pureNote: $$(note), pureNote: note,
isDeletedRef: isDeleted, isDeletedRef: isDeleted,
}); });
useTooltip(renoteButton, async (showing) => { useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', { const renotes = await os.api('notes/renotes', {
noteId: appearNote.id, noteId: appearNote.value.id,
limit: 11, limit: 11,
}); });
@@ -339,7 +339,7 @@ useTooltip(renoteButton, async (showing) => {
os.popup(MkUsersTooltip, { os.popup(MkUsersTooltip, {
showing, showing,
users, users,
count: appearNote.renoteCount, count: appearNote.value.renoteCount,
targetElement: renoteButton.value, targetElement: renoteButton.value,
}, {}, 'closed'); }, {}, 'closed');
}); });
@@ -348,7 +348,7 @@ function renote(viaKeyboard = false) {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
const { menu } = getRenoteMenu({ note: note, renoteButton }); const { menu } = getRenoteMenu({ note: note.value, renoteButton });
os.popupMenu(menu, renoteButton.value, { os.popupMenu(menu, renoteButton.value, {
viaKeyboard, viaKeyboard,
}); });
@@ -358,8 +358,8 @@ function reply(viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
os.post({ os.post({
reply: appearNote, reply: appearNote.value,
channel: appearNote.channel, channel: appearNote.value.channel,
animation: !viaKeyboard, animation: !viaKeyboard,
}, () => { }, () => {
focus(); focus();
@@ -369,11 +369,11 @@ function reply(viaKeyboard = false): void {
function react(viaKeyboard = false): void { function react(viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
if (appearNote.reactionAcceptance === 'likeOnly') { if (appearNote.value.reactionAcceptance === 'likeOnly') {
sound.play('reaction'); sound.play('reaction');
os.api('notes/reactions/create', { os.api('notes/reactions/create', {
noteId: appearNote.id, noteId: appearNote.value.id,
reaction: '❤️', reaction: '❤️',
}); });
const el = reactButton.value as HTMLElement | null | undefined; const el = reactButton.value as HTMLElement | null | undefined;
@@ -389,10 +389,10 @@ function react(viaKeyboard = false): void {
sound.play('reaction'); sound.play('reaction');
os.api('notes/reactions/create', { os.api('notes/reactions/create', {
noteId: appearNote.id, noteId: appearNote.value.id,
reaction: reaction, reaction: reaction,
}); });
if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead'); claimAchievement('reactWithoutRead');
} }
}, () => { }, () => {
@@ -423,20 +423,20 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault(); ev.preventDefault();
react(); react();
} else { } else {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }); const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
os.contextMenu(menu, ev).then(focus).finally(cleanup); os.contextMenu(menu, ev).then(focus).finally(cleanup);
} }
} }
function menu(viaKeyboard = false): void { function menu(viaKeyboard = false): void {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }); const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
os.popupMenu(menu, menuButton.value, { os.popupMenu(menu, menuButton.value, {
viaKeyboard, viaKeyboard,
}).then(focus).finally(cleanup); }).then(focus).finally(cleanup);
} }
async function clip() { async function clip() {
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus); os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
} }
function showRenoteMenu(viaKeyboard = false): void { function showRenoteMenu(viaKeyboard = false): void {
@@ -448,7 +448,7 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true, danger: true,
action: () => { action: () => {
os.api('notes/delete', { os.api('notes/delete', {
noteId: note.id, noteId: note.value.id,
}); });
isDeleted.value = true; isDeleted.value = true;
}, },
@@ -470,7 +470,7 @@ const repliesLoaded = ref(false);
function loadReplies() { function loadReplies() {
repliesLoaded.value = true; repliesLoaded.value = true;
os.api('notes/children', { os.api('notes/children', {
noteId: appearNote.id, noteId: appearNote.value.id,
limit: 30, limit: 30,
}).then(res => { }).then(res => {
replies.value = res; replies.value = res;
@@ -482,7 +482,7 @@ const conversationLoaded = ref(false);
function loadConversation() { function loadConversation() {
conversationLoaded.value = true; conversationLoaded.value = true;
os.api('notes/conversation', { os.api('notes/conversation', {
noteId: appearNote.replyId, noteId: appearNote.value.replyId,
}).then(res => { }).then(res => {
conversation.value = res.reverse(); conversation.value = res.reverse();
}); });

View File

@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue'; import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
@@ -33,7 +33,7 @@ const props = defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
}>(); }>();
const showContent = $ref(false); const showContent = ref(false);
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@@ -65,15 +65,15 @@ const props = withDefaults(defineProps<{
const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false); const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
let showContent = $ref(false); const showContent = ref(false);
let replies: Misskey.entities.Note[] = $ref([]); const replies = ref<Misskey.entities.Note[]>([]);
if (props.detail) { if (props.detail) {
os.api('notes/children', { os.api('notes/children', {
noteId: props.note.id, noteId: props.note.id,
limit: 5, limit: 5,
}).then(res => { }).then(res => {
replies = res; replies.value = res;
}); });
} }
</script> </script>

View File

@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, Ref } from 'vue'; import { ref, Ref, shallowRef } from 'vue';
import MkSwitch from './MkSwitch.vue'; import MkSwitch from './MkSwitch.vue';
import MkInfo from './MkInfo.vue'; import MkInfo from './MkInfo.vue';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
@@ -51,7 +51,7 @@ const props = withDefaults(defineProps<{
excludeTypes: () => [], excludeTypes: () => [],
}); });
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>(); const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any); const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any);
@@ -61,7 +61,7 @@ function ok() {
.filter(type => !typesMap[type].value), .filter(type => !typesMap[type].value),
}); });
if (dialog) dialog.close(); if (dialog.value) dialog.value.close();
} }
function disableAll() { function disableAll() {

View File

@@ -43,7 +43,7 @@ const props = defineProps<{
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
let pagination = $computed(() => defaultStore.reactiveState.useGroupedNotifications.value ? { const pagination = computed(() => defaultStore.reactiveState.useGroupedNotifications.value ? {
endpoint: 'i/notifications-grouped' as const, endpoint: 'i/notifications-grouped' as const,
limit: 20, limit: 20,
params: computed(() => ({ params: computed(() => ({

View File

@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue'; import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@@ -22,13 +22,13 @@ const props = withDefaults(defineProps<{
maxHeight: 200, maxHeight: 200,
}); });
let content = $shallowRef<HTMLElement>(); const content = shallowRef<HTMLElement>();
let omitted = $ref(false); const omitted = ref(false);
let ignoreOmit = $ref(false); const ignoreOmit = ref(false);
const calcOmit = () => { const calcOmit = () => {
if (omitted || ignoreOmit) return; if (omitted.value || ignoreOmit.value) return;
omitted = content.offsetHeight > props.maxHeight; omitted.value = content.value.offsetHeight > props.maxHeight;
}; };
const omitObserver = new ResizeObserver((entries, observer) => { const omitObserver = new ResizeObserver((entries, observer) => {
@@ -37,7 +37,7 @@ const omitObserver = new ResizeObserver((entries, observer) => {
onMounted(() => { onMounted(() => {
calcOmit(); calcOmit();
omitObserver.observe(content); omitObserver.observe(content.value);
}); });
onUnmounted(() => { onUnmounted(() => {

View File

@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ComputedRef, onMounted, onUnmounted, provide, shallowRef } from 'vue'; import { ComputedRef, onMounted, onUnmounted, provide, shallowRef, ref, computed } from 'vue';
import RouterView from '@/components/global/RouterView.vue'; import RouterView from '@/components/global/RouterView.vue';
import MkWindow from '@/components/MkWindow.vue'; import MkWindow from '@/components/MkWindow.vue';
import { popout as _popout } from '@/scripts/popout.js'; import { popout as _popout } from '@/scripts/popout.js';
@@ -55,16 +55,16 @@ defineEmits<{
const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue'))); const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue')));
const contents = shallowRef<HTMLElement>(); const contents = shallowRef<HTMLElement>();
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
let windowEl = $shallowRef<InstanceType<typeof MkWindow>>(); const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
const history = $ref<{ path: string; key: any; }[]>([{ const history = ref<{ path: string; key: any; }[]>([{
path: router.getCurrentPath(), path: router.getCurrentPath(),
key: router.getCurrentKey(), key: router.getCurrentKey(),
}]); }]);
const buttonsLeft = $computed(() => { const buttonsLeft = computed(() => {
const buttons = []; const buttons = [];
if (history.length > 1) { if (history.value.length > 1) {
buttons.push({ buttons.push({
icon: 'ti ti-arrow-left', icon: 'ti ti-arrow-left',
onClick: back, onClick: back,
@@ -73,7 +73,7 @@ const buttonsLeft = $computed(() => {
return buttons; return buttons;
}); });
const buttonsRight = $computed(() => { const buttonsRight = computed(() => {
const buttons = [{ const buttons = [{
icon: 'ti ti-reload', icon: 'ti ti-reload',
title: i18n.ts.reload, title: i18n.ts.reload,
@@ -86,21 +86,21 @@ const buttonsRight = $computed(() => {
return buttons; return buttons;
}); });
let reloadCount = $ref(0); const reloadCount = ref(0);
router.addListener('push', ctx => { router.addListener('push', ctx => {
history.push({ path: ctx.path, key: ctx.key }); history.value.push({ path: ctx.path, key: ctx.key });
}); });
provide('router', router); provide('router', router);
provideMetadataReceiver((info) => { provideMetadataReceiver((info) => {
pageMetadata = info; pageMetadata.value = info;
}); });
provide('shouldOmitHeaderTitle', true); provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true); provide('shouldHeaderThin', true);
provide('forceSpacerMin', true); provide('forceSpacerMin', true);
const contextmenu = $computed(() => ([{ const contextmenu = computed(() => ([{
icon: 'ti ti-player-eject', icon: 'ti ti-player-eject',
text: i18n.ts.showInPage, text: i18n.ts.showInPage,
action: expand, action: expand,
@@ -112,8 +112,8 @@ const contextmenu = $computed(() => ([{
icon: 'ti ti-external-link', icon: 'ti ti-external-link',
text: i18n.ts.openInNewTab, text: i18n.ts.openInNewTab,
action: () => { action: () => {
window.open(url + router.getCurrentPath(), '_blank'); window.open(url + router.getCurrentPath(), '_blank', 'noopener');
windowEl.close(); windowEl.value.close();
}, },
}, { }, {
icon: 'ti ti-link', icon: 'ti ti-link',
@@ -124,26 +124,26 @@ const contextmenu = $computed(() => ([{
}])); }]));
function back() { function back() {
history.pop(); history.value.pop();
router.replace(history.at(-1)!.path, history.at(-1)!.key); router.replace(history.value.at(-1)!.path, history.value.at(-1)!.key);
} }
function reload() { function reload() {
reloadCount++; reloadCount.value++;
} }
function close() { function close() {
windowEl.close(); windowEl.value.close();
} }
function expand() { function expand() {
mainRouter.push(router.getCurrentPath(), 'forcePage'); mainRouter.push(router.getCurrentPath(), 'forcePage');
windowEl.close(); windowEl.value.close();
} }
function popout() { function popout() {
_popout(router.getCurrentPath(), windowEl.$el); _popout(router.getCurrentPath(), windowEl.value.$el);
windowEl.close(); windowEl.value.close();
} }
useScrollPositionManager(() => getScrollContainer(contents.value), router); useScrollPositionManager(() => getScrollContainer(contents.value), router);

View File

@@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, watch } from 'vue'; import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js'; import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js';
@@ -105,12 +105,12 @@ const emit = defineEmits<{
(ev: 'status', error: boolean): void; (ev: 'status', error: boolean): void;
}>(); }>();
let rootEl = $shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
// 遡り中かどうか // 遡り中かどうか
let backed = $ref(false); const backed = ref(false);
let scrollRemove = $ref<(() => void) | null>(null); const scrollRemove = ref<(() => void) | null>(null);
/** /**
* 表示するアイテムのソース * 表示するアイテムのソース
@@ -142,8 +142,8 @@ const {
enableInfiniteScroll, enableInfiniteScroll,
} = defaultStore.reactiveState; } = defaultStore.reactiveState;
const contentEl = $computed(() => props.pagination.pageEl ?? rootEl); const contentEl = computed(() => props.pagination.pageEl ?? rootEl.value);
const scrollableElement = $computed(() => contentEl ? getScrollContainer(contentEl) : document.body); const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : document.body);
const visibility = useDocumentVisibility(); const visibility = useDocumentVisibility();
@@ -153,35 +153,35 @@ const BACKGROUND_PAUSE_WAIT_SEC = 10;
// 先頭が表示されているかどうかを検出 // 先頭が表示されているかどうかを検出
// https://qiita.com/mkataigi/items/0154aefd2223ce23398e // https://qiita.com/mkataigi/items/0154aefd2223ce23398e
let scrollObserver = $ref<IntersectionObserver>(); const scrollObserver = ref<IntersectionObserver>();
watch([() => props.pagination.reversed, $$(scrollableElement)], () => { watch([() => props.pagination.reversed, scrollableElement], () => {
if (scrollObserver) scrollObserver.disconnect(); if (scrollObserver.value) scrollObserver.value.disconnect();
scrollObserver = new IntersectionObserver(entries => { scrollObserver.value = new IntersectionObserver(entries => {
backed = entries[0].isIntersecting; backed.value = entries[0].isIntersecting;
}, { }, {
root: scrollableElement, root: scrollableElement.value,
rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px', rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px',
threshold: 0.01, threshold: 0.01,
}); });
}, { immediate: true }); }, { immediate: true });
watch($$(rootEl), () => { watch(rootEl, () => {
scrollObserver?.disconnect(); scrollObserver.value?.disconnect();
nextTick(() => { nextTick(() => {
if (rootEl) scrollObserver?.observe(rootEl); if (rootEl.value) scrollObserver.value?.observe(rootEl.value);
}); });
}); });
watch([$$(backed), $$(contentEl)], () => { watch([backed, contentEl], () => {
if (!backed) { if (!backed.value) {
if (!contentEl) return; if (!contentEl.value) return;
scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, executeQueue, TOLERANCE); scrollRemove.value = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE);
} else { } else {
if (scrollRemove) scrollRemove(); if (scrollRemove.value) scrollRemove.value();
scrollRemove = null; scrollRemove.value = null;
} }
}); });
@@ -254,14 +254,14 @@ const fetchMore = async (): Promise<void> => {
} }
const reverseConcat = _res => { const reverseConcat = _res => {
const oldHeight = scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight(); const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight();
const oldScroll = scrollableElement ? scrollableElement.scrollTop : window.scrollY; const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY;
items.value = concatMapWithArray(items.value, _res); items.value = concatMapWithArray(items.value, _res);
return nextTick(() => { return nextTick(() => {
if (scrollableElement) { if (scrollableElement.value) {
scroll(scrollableElement, { top: oldScroll + (scrollableElement.scrollHeight - oldHeight), behavior: 'instant' }); scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
} else { } else {
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' }); window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
} }
@@ -351,7 +351,7 @@ const appearFetchMoreAhead = async (): Promise<void> => {
fetchMoreAppearTimeout(); fetchMoreAppearTimeout();
}; };
const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl!, TOLERANCE); const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE);
watch(visibility, () => { watch(visibility, () => {
if (visibility.value === 'hidden') { if (visibility.value === 'hidden') {
@@ -445,11 +445,11 @@ onActivated(() => {
}); });
onDeactivated(() => { onDeactivated(() => {
isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl ? rootEl.scrollHeight - window.innerHeight : 0) : window.scrollY === 0; isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl.value ? rootEl.value.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
}); });
function toBottom() { function toBottom() {
scrollToBottom(contentEl!); scrollToBottom(contentEl.value!);
} }
onBeforeMount(() => { onBeforeMount(() => {
@@ -477,13 +477,13 @@ onBeforeUnmount(() => {
clearTimeout(preventAppearFetchMoreTimer.value); clearTimeout(preventAppearFetchMoreTimer.value);
preventAppearFetchMoreTimer.value = null; preventAppearFetchMoreTimer.value = null;
} }
scrollObserver?.disconnect(); scrollObserver.value?.disconnect();
}); });
defineExpose({ defineExpose({
items, items,
queue, queue,
backed, backed: backed.value,
more, more,
reload, reload,
prepend, prepend,

View File

@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted, shallowRef, ref } from 'vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
@@ -49,22 +49,22 @@ const emit = defineEmits<{
(ev: 'cancelled'): void; (ev: 'cancelled'): void;
}>(); }>();
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>(); const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const passwordInput = $shallowRef<InstanceType<typeof MkInput>>(); const passwordInput = shallowRef<InstanceType<typeof MkInput>>();
const password = $ref(''); const password = ref('');
const token = $ref(null); const token = ref(null);
function onClose() { function onClose() {
emit('cancelled'); emit('cancelled');
if (dialog) dialog.close(); if (dialog.value) dialog.value.close();
} }
function done(res) { function done(res) {
emit('done', { password, token }); emit('done', { password: password.value, token: token.value });
if (dialog) dialog.close(); if (dialog.value) dialog.value.close();
} }
onMounted(() => { onMounted(() => {
if (passwordInput) passwordInput.focus(); if (passwordInput.value) passwordInput.value.focus();
}); });
</script> </script>

View File

@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted, ref } from 'vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@@ -23,13 +23,13 @@ const emit = defineEmits<{
(ev: 'end'): void; (ev: 'end'): void;
}>(); }>();
let up = $ref(false); const up = ref(false);
const zIndex = os.claimZIndex('middle'); const zIndex = os.claimZIndex('middle');
const angle = (45 - (Math.random() * 90)) + 'deg'; const angle = (45 - (Math.random() * 90)) + 'deg';
onMounted(() => { onMounted(() => {
window.setTimeout(() => { window.setTimeout(() => {
up = true; up.value = true;
}, 10); }, 10);
window.setTimeout(() => { window.setTimeout(() => {

View File

@@ -10,10 +10,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref, shallowRef } from 'vue';
import MkModal from './MkModal.vue'; import MkModal from './MkModal.vue';
import MkMenu from './MkMenu.vue'; import MkMenu from './MkMenu.vue';
import { MenuItem } from '@/types/menu'; import { MenuItem } from '@/types/menu.js';
defineProps<{ defineProps<{
items: MenuItem[]; items: MenuItem[];
@@ -28,7 +28,7 @@ const emit = defineEmits<{
(ev: 'closing'): void; (ev: 'closing'): void;
}>(); }>();
let modal = $shallowRef<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
const manualShowing = ref(true); const manualShowing = ref(true);
const hiding = ref(false); const hiding = ref(false);
@@ -60,14 +60,14 @@ function hide() {
hiding.value = true; hiding.value = true;
// closeは呼ぶ必要がある // closeは呼ぶ必要がある
modal?.close(); modal.value?.close();
} }
function close() { function close() {
manualShowing.value = false; manualShowing.value = false;
// closeは呼ぶ必要がある // closeは呼ぶ必要がある
modal?.close(); modal.value?.close();
} }
</script> </script>

View File

@@ -98,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, ref } from 'vue'; import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor'; import insertTextAtCursor from 'insert-text-at-cursor';
@@ -135,6 +135,7 @@ const props = withDefaults(defineProps<{
mention?: Misskey.entities.User; mention?: Misskey.entities.User;
specified?: Misskey.entities.User; specified?: Misskey.entities.User;
initialText?: string; initialText?: string;
initialCw?: string;
initialVisibility?: (typeof Misskey.noteVisibilities)[number]; initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialFiles?: Misskey.entities.DriveFile[]; initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean; initialLocalOnly?: boolean;
@@ -162,42 +163,42 @@ const emit = defineEmits<{
(ev: 'fileChangeSensitive', fileId: string, to: boolean): void; (ev: 'fileChangeSensitive', fileId: string, to: boolean): void;
}>(); }>();
const textareaEl = $shallowRef<HTMLTextAreaElement | null>(null); const textareaEl = shallowRef<HTMLTextAreaElement | null>(null);
const cwInputEl = $shallowRef<HTMLInputElement | null>(null); const cwInputEl = shallowRef<HTMLInputElement | null>(null);
const hashtagsInputEl = $shallowRef<HTMLInputElement | null>(null); const hashtagsInputEl = shallowRef<HTMLInputElement | null>(null);
const visibilityButton = $shallowRef<HTMLElement | null>(null); const visibilityButton = shallowRef<HTMLElement | null>(null);
let posting = $ref(false); const posting = ref(false);
let posted = $ref(false); const posted = ref(false);
let text = $ref(props.initialText ?? ''); const text = ref(props.initialText ?? '');
let files = $ref(props.initialFiles ?? []); const files = ref(props.initialFiles ?? []);
let poll = $ref<{ const poll = ref<{
choices: string[]; choices: string[];
multiple: boolean; multiple: boolean;
expiresAt: string | null; expiresAt: string | null;
expiredAfter: string | null; expiredAfter: string | null;
} | null>(null); } | null>(null);
let useCw = $ref(false); const useCw = ref<boolean>(!!props.initialCw);
let showPreview = $ref(defaultStore.state.showPreview); const showPreview = ref(defaultStore.state.showPreview);
watch($$(showPreview), () => defaultStore.set('showPreview', showPreview)); watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
let cw = $ref<string | null>(null); const cw = ref<string | null>(props.initialCw ?? null);
let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly); const localOnly = ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof Misskey.noteVisibilities[number]); const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof Misskey.noteVisibilities[number]);
let visibleUsers = $ref([]); const visibleUsers = ref([]);
if (props.initialVisibleUsers) { if (props.initialVisibleUsers) {
props.initialVisibleUsers.forEach(pushVisibleUser); props.initialVisibleUsers.forEach(pushVisibleUser);
} }
let reactionAcceptance = $ref(defaultStore.state.reactionAcceptance); const reactionAcceptance = ref(defaultStore.state.reactionAcceptance);
let autocomplete = $ref(null); const autocomplete = ref(null);
let draghover = $ref(false); const draghover = ref(false);
let quoteId = $ref(null); const quoteId = ref(null);
let hasNotSpecifiedMentions = $ref(false); const hasNotSpecifiedMentions = ref(false);
let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]')); const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'));
let imeText = $ref(''); const imeText = ref('');
let showingOptions = $ref(false); const showingOptions = ref(false);
const textAreaReadOnly = ref(false); const textAreaReadOnly = ref(false);
const draftKey = $computed((): string => { const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : ''; let key = props.channel ? `channel:${props.channel.id}` : '';
if (props.renote) { if (props.renote) {
@@ -211,7 +212,7 @@ const draftKey = $computed((): string => {
return key; return key;
}); });
const placeholder = $computed((): string => { const placeholder = computed((): string => {
if (props.renote) { if (props.renote) {
return i18n.ts._postForm.quotePlaceholder; return i18n.ts._postForm.quotePlaceholder;
} else if (props.reply) { } else if (props.reply) {
@@ -231,7 +232,7 @@ const placeholder = $computed((): string => {
} }
}); });
const submitText = $computed((): string => { const submitText = computed((): string => {
return props.renote return props.renote
? i18n.ts.quote ? i18n.ts.quote
: props.reply : props.reply
@@ -239,45 +240,45 @@ const submitText = $computed((): string => {
: i18n.ts.note; : i18n.ts.note;
}); });
const textLength = $computed((): number => { const textLength = computed((): number => {
return (text + imeText).trim().length; return (text.value + imeText.value).trim().length;
}); });
const maxTextLength = $computed((): number => { const maxTextLength = computed((): number => {
return instance ? instance.maxNoteTextLength : 1000; return instance ? instance.maxNoteTextLength : 1000;
}); });
const canPost = $computed((): boolean => { const canPost = computed((): boolean => {
return !props.mock && !posting && !posted && return !props.mock && !posting.value && !posted.value &&
(1 <= textLength || 1 <= files.length || !!poll || !!props.renote) && (1 <= textLength.value || 1 <= files.value.length || !!poll.value || !!props.renote) &&
(textLength <= maxTextLength) && (textLength.value <= maxTextLength.value) &&
(!poll || poll.choices.length >= 2); (!poll.value || poll.value.choices.length >= 2);
}); });
const withHashtags = $computed(defaultStore.makeGetterSetter('postFormWithHashtags')); const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
const hashtags = $computed(defaultStore.makeGetterSetter('postFormHashtags')); const hashtags = computed(defaultStore.makeGetterSetter('postFormHashtags'));
watch($$(text), () => { watch(text, () => {
checkMissingMention(); checkMissingMention();
}, { immediate: true }); }, { immediate: true });
watch($$(visibility), () => { watch(visibility, () => {
checkMissingMention(); checkMissingMention();
}, { immediate: true }); }, { immediate: true });
watch($$(visibleUsers), () => { watch(visibleUsers, () => {
checkMissingMention(); checkMissingMention();
}, { }, {
deep: true, deep: true,
}); });
if (props.mention) { if (props.mention) {
text = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`; text.value = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`;
text += ' '; text.value += ' ';
} }
if (props.reply && (props.reply.user.username !== $i.username || (props.reply.user.host != null && props.reply.user.host !== host))) { if (props.reply && (props.reply.user.username !== $i.username || (props.reply.user.host != null && props.reply.user.host !== host))) {
text = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `; text.value = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `;
} }
if (props.reply && props.reply.text != null) { if (props.reply && props.reply.text != null) {
@@ -295,32 +296,32 @@ if (props.reply && props.reply.text != null) {
if ($i.username === x.username && (x.host == null || x.host === host)) continue; if ($i.username === x.username && (x.host == null || x.host === host)) continue;
// 重複は除外 // 重複は除外
if (text.includes(`${mention} `)) continue; if (text.value.includes(`${mention} `)) continue;
text += `${mention} `; text.value += `${mention} `;
} }
} }
if ($i?.isSilenced && visibility === 'public') { if ($i?.isSilenced && visibility.value === 'public') {
visibility = 'home'; visibility.value = 'home';
} }
if (props.channel) { if (props.channel) {
visibility = 'public'; visibility.value = 'public';
localOnly = true; // TODO: チャンネルが連合するようになった折には消す localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
} }
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ // 公開以外へのリプライ時は元の公開範囲を引き継ぐ
if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) { if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) {
if (props.reply.visibility === 'home' && visibility === 'followers') { if (props.reply.visibility === 'home' && visibility.value === 'followers') {
visibility = 'followers'; visibility.value = 'followers';
} else if (['home', 'followers'].includes(props.reply.visibility) && visibility === 'specified') { } else if (['home', 'followers'].includes(props.reply.visibility) && visibility.value === 'specified') {
visibility = 'specified'; visibility.value = 'specified';
} else { } else {
visibility = props.reply.visibility; visibility.value = props.reply.visibility;
} }
if (visibility === 'specified') { if (visibility.value === 'specified') {
if (props.reply.visibleUserIds) { if (props.reply.visibleUserIds) {
os.api('users/show', { os.api('users/show', {
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId), userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId),
@@ -338,57 +339,57 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
} }
if (props.specified) { if (props.specified) {
visibility = 'specified'; visibility.value = 'specified';
pushVisibleUser(props.specified); pushVisibleUser(props.specified);
} }
// keep cw when reply // keep cw when reply
if (defaultStore.state.keepCw && props.reply && props.reply.cw) { if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
useCw = true; useCw.value = true;
cw = props.reply.cw; cw.value = props.reply.cw;
} }
function watchForDraft() { function watchForDraft() {
watch($$(text), () => saveDraft()); watch(text, () => saveDraft());
watch($$(useCw), () => saveDraft()); watch(useCw, () => saveDraft());
watch($$(cw), () => saveDraft()); watch(cw, () => saveDraft());
watch($$(poll), () => saveDraft()); watch(poll, () => saveDraft());
watch($$(files), () => saveDraft(), { deep: true }); watch(files, () => saveDraft(), { deep: true });
watch($$(visibility), () => saveDraft()); watch(visibility, () => saveDraft());
watch($$(localOnly), () => saveDraft()); watch(localOnly, () => saveDraft());
} }
function checkMissingMention() { function checkMissingMention() {
if (visibility === 'specified') { if (visibility.value === 'specified') {
const ast = mfm.parse(text); const ast = mfm.parse(text.value);
for (const x of extractMentions(ast)) { for (const x of extractMentions(ast)) {
if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) { if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) {
hasNotSpecifiedMentions = true; hasNotSpecifiedMentions.value = true;
return; return;
} }
} }
} }
hasNotSpecifiedMentions = false; hasNotSpecifiedMentions.value = false;
} }
function addMissingMention() { function addMissingMention() {
const ast = mfm.parse(text); const ast = mfm.parse(text.value);
for (const x of extractMentions(ast)) { for (const x of extractMentions(ast)) {
if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) { if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) {
os.api('users/show', { username: x.username, host: x.host }).then(user => { os.api('users/show', { username: x.username, host: x.host }).then(user => {
visibleUsers.push(user); visibleUsers.value.push(user);
}); });
} }
} }
} }
function togglePoll() { function togglePoll() {
if (poll) { if (poll.value) {
poll = null; poll.value = null;
} else { } else {
poll = { poll.value = {
choices: ['', ''], choices: ['', ''],
multiple: false, multiple: false,
expiresAt: null, expiresAt: null,
@@ -398,13 +399,13 @@ function togglePoll() {
} }
function addTag(tag: string) { function addTag(tag: string) {
insertTextAtCursor(textareaEl, ` #${tag} `); insertTextAtCursor(textareaEl.value, ` #${tag} `);
} }
function focus() { function focus() {
if (textareaEl) { if (textareaEl.value) {
textareaEl.focus(); textareaEl.value.focus();
textareaEl.setSelectionRange(textareaEl.value.length, textareaEl.value.length); textareaEl.value.setSelectionRange(textareaEl.value.value.length, textareaEl.value.value.length);
} }
} }
@@ -413,55 +414,55 @@ function chooseFileFrom(ev) {
selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => { selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
for (const file of files_) { for (const file of files_) {
files.push(file); files.value.push(file);
} }
}); });
} }
function detachFile(id) { function detachFile(id) {
files = files.filter(x => x.id !== id); files.value = files.value.filter(x => x.id !== id);
} }
function updateFileSensitive(file, sensitive) { function updateFileSensitive(file, sensitive) {
if (props.mock) { if (props.mock) {
emit('fileChangeSensitive', file.id, sensitive); emit('fileChangeSensitive', file.id, sensitive);
} }
files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive; files.value[files.value.findIndex(x => x.id === file.id)].isSensitive = sensitive;
} }
function updateFileName(file, name) { function updateFileName(file, name) {
files[files.findIndex(x => x.id === file.id)].name = name; files.value[files.value.findIndex(x => x.id === file.id)].name = name;
} }
function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities.DriveFile): void { function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities.DriveFile): void {
files[files.findIndex(x => x.id === file.id)] = newFile; files.value[files.value.findIndex(x => x.id === file.id)] = newFile;
} }
function upload(file: File, name?: string): void { function upload(file: File, name?: string): void {
if (props.mock) return; if (props.mock) return;
uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
files.push(res); files.value.push(res);
}); });
} }
function setVisibility() { function setVisibility() {
if (props.channel) { if (props.channel) {
visibility = 'public'; visibility.value = 'public';
localOnly = true; // TODO: チャンネルが連合するようになった折には消す localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
return; return;
} }
os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
currentVisibility: visibility, currentVisibility: visibility.value,
isSilenced: $i?.isSilenced, isSilenced: $i?.isSilenced,
localOnly: localOnly, localOnly: localOnly.value,
src: visibilityButton, src: visibilityButton.value,
}, { }, {
changeVisibility: v => { changeVisibility: v => {
visibility = v; visibility.value = v;
if (defaultStore.state.rememberNoteVisibility) { if (defaultStore.state.rememberNoteVisibility) {
defaultStore.set('visibility', visibility); defaultStore.set('visibility', visibility.value);
} }
}, },
}, 'closed'); }, 'closed');
@@ -469,14 +470,14 @@ function setVisibility() {
async function toggleLocalOnly() { async function toggleLocalOnly() {
if (props.channel) { if (props.channel) {
visibility = 'public'; visibility.value = 'public';
localOnly = true; // TODO: チャンネルが連合するようになった折には消す localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
return; return;
} }
const neverShowInfo = miLocalStorage.getItem('neverShowLocalOnlyInfo'); const neverShowInfo = miLocalStorage.getItem('neverShowLocalOnlyInfo');
if (!localOnly && neverShowInfo !== 'true') { if (!localOnly.value && neverShowInfo !== 'true') {
const confirm = await os.actions({ const confirm = await os.actions({
type: 'question', type: 'question',
title: i18n.ts.disableFederationConfirm, title: i18n.ts.disableFederationConfirm,
@@ -506,7 +507,7 @@ async function toggleLocalOnly() {
} }
} }
localOnly = !localOnly; localOnly.value = !localOnly.value;
} }
async function toggleReactionAcceptance() { async function toggleReactionAcceptance() {
@@ -519,15 +520,15 @@ async function toggleReactionAcceptance() {
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }, { value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
{ value: 'likeOnly' as const, text: i18n.ts.likeOnly }, { value: 'likeOnly' as const, text: i18n.ts.likeOnly },
], ],
default: reactionAcceptance, default: reactionAcceptance.value,
}); });
if (select.canceled) return; if (select.canceled) return;
reactionAcceptance = select.result; reactionAcceptance.value = select.result;
} }
function pushVisibleUser(user) { function pushVisibleUser(user) {
if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) { if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) {
visibleUsers.push(user); visibleUsers.value.push(user);
} }
} }
@@ -535,34 +536,34 @@ function addVisibleUser() {
os.selectUser().then(user => { os.selectUser().then(user => {
pushVisibleUser(user); pushVisibleUser(user);
if (!text.toLowerCase().includes(`@${user.username.toLowerCase()}`)) { if (!text.value.toLowerCase().includes(`@${user.username.toLowerCase()}`)) {
text = `@${Misskey.acct.toString(user)} ${text}`; text.value = `@${Misskey.acct.toString(user)} ${text.value}`;
} }
}); });
} }
function removeVisibleUser(user) { function removeVisibleUser(user) {
visibleUsers = erase(user, visibleUsers); visibleUsers.value = erase(user, visibleUsers.value);
} }
function clear() { function clear() {
text = ''; text.value = '';
files = []; files.value = [];
poll = null; poll.value = null;
quoteId = null; quoteId.value = null;
} }
function onKeydown(ev: KeyboardEvent) { function onKeydown(ev: KeyboardEvent) {
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost) post(); if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
if (ev.key === 'Escape') emit('esc'); if (ev.key === 'Escape') emit('esc');
} }
function onCompositionUpdate(ev: CompositionEvent) { function onCompositionUpdate(ev: CompositionEvent) {
imeText = ev.data; imeText.value = ev.data;
} }
function onCompositionEnd(ev: CompositionEvent) { function onCompositionEnd(ev: CompositionEvent) {
imeText = ''; imeText.value = '';
} }
async function onPaste(ev: ClipboardEvent) { async function onPaste(ev: ClipboardEvent) {
@@ -580,7 +581,7 @@ async function onPaste(ev: ClipboardEvent) {
const paste = ev.clipboardData.getData('text'); const paste = ev.clipboardData.getData('text');
if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) { if (!props.renote && !quoteId.value && paste.startsWith(url + '/notes/')) {
ev.preventDefault(); ev.preventDefault();
os.confirm({ os.confirm({
@@ -588,11 +589,11 @@ async function onPaste(ev: ClipboardEvent) {
text: i18n.ts.quoteQuestion, text: i18n.ts.quoteQuestion,
}).then(({ canceled }) => { }).then(({ canceled }) => {
if (canceled) { if (canceled) {
insertTextAtCursor(textareaEl, paste); insertTextAtCursor(textareaEl.value, paste);
return; return;
} }
quoteId = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1]; quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
}); });
} }
} }
@@ -603,7 +604,7 @@ function onDragover(ev) {
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
if (isFile || isDriveFile) { if (isFile || isDriveFile) {
ev.preventDefault(); ev.preventDefault();
draghover = true; draghover.value = true;
switch (ev.dataTransfer.effectAllowed) { switch (ev.dataTransfer.effectAllowed) {
case 'all': case 'all':
case 'uninitialized': case 'uninitialized':
@@ -624,15 +625,15 @@ function onDragover(ev) {
} }
function onDragenter(ev) { function onDragenter(ev) {
draghover = true; draghover.value = true;
} }
function onDragleave(ev) { function onDragleave(ev) {
draghover = false; draghover.value = false;
} }
function onDrop(ev): void { function onDrop(ev): void {
draghover = false; draghover.value = false;
// ファイルだったら // ファイルだったら
if (ev.dataTransfer.files.length > 0) { if (ev.dataTransfer.files.length > 0) {
@@ -645,7 +646,7 @@ function onDrop(ev): void {
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== '') { if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile); const file = JSON.parse(driveFile);
files.push(file); files.value.push(file);
ev.preventDefault(); ev.preventDefault();
} }
//#endregion //#endregion
@@ -656,16 +657,16 @@ function saveDraft() {
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
draftData[draftKey] = { draftData[draftKey.value] = {
updatedAt: new Date(), updatedAt: new Date(),
data: { data: {
text: text, text: text.value,
useCw: useCw, useCw: useCw.value,
cw: cw, cw: cw.value,
visibility: visibility, visibility: visibility.value,
localOnly: localOnly, localOnly: localOnly.value,
files: files, files: files.value,
poll: poll, poll: poll.value,
}, },
}; };
@@ -675,13 +676,13 @@ function saveDraft() {
function deleteDraft() { function deleteDraft() {
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
delete draftData[draftKey]; delete draftData[draftKey.value];
miLocalStorage.setItem('drafts', JSON.stringify(draftData)); miLocalStorage.setItem('drafts', JSON.stringify(draftData));
} }
async function post(ev?: MouseEvent) { async function post(ev?: MouseEvent) {
if (useCw && (cw == null || cw.trim() === '')) { if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
os.alert({ os.alert({
type: 'error', type: 'error',
text: i18n.ts.cwNotationRequired, text: i18n.ts.cwNotationRequired,
@@ -700,13 +701,13 @@ async function post(ev?: MouseEvent) {
if (props.mock) return; if (props.mock) return;
const annoying = const annoying =
text.includes('$[x2') || text.value.includes('$[x2') ||
text.includes('$[x3') || text.value.includes('$[x3') ||
text.includes('$[x4') || text.value.includes('$[x4') ||
text.includes('$[scale') || text.value.includes('$[scale') ||
text.includes('$[position'); text.value.includes('$[position');
if (annoying && visibility === 'public') { if (annoying && visibility.value === 'public') {
const { canceled, result } = await os.actions({ const { canceled, result } = await os.actions({
type: 'warning', type: 'warning',
text: i18n.ts.thisPostMayBeAnnoying, text: i18n.ts.thisPostMayBeAnnoying,
@@ -726,26 +727,26 @@ async function post(ev?: MouseEvent) {
if (canceled) return; if (canceled) return;
if (result === 'cancel') return; if (result === 'cancel') return;
if (result === 'home') { if (result === 'home') {
visibility = 'home'; visibility.value = 'home';
} }
} }
let postData = { let postData = {
text: text === '' ? null : text, text: text.value === '' ? null : text.value,
fileIds: files.length > 0 ? files.map(f => f.id) : undefined, fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
replyId: props.reply ? props.reply.id : undefined, replyId: props.reply ? props.reply.id : undefined,
renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined, renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined,
channelId: props.channel ? props.channel.id : undefined, channelId: props.channel ? props.channel.id : undefined,
poll: poll, poll: poll.value,
cw: useCw ? cw ?? '' : null, cw: useCw.value ? cw.value ?? '' : null,
localOnly: localOnly, localOnly: localOnly.value,
visibility: visibility, visibility: visibility.value,
visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined, visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
reactionAcceptance, reactionAcceptance: reactionAcceptance.value,
}; };
if (withHashtags && hashtags && hashtags.trim() !== '') { if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') {
const hashtags_ = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); const hashtags_ = hashtags.value.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_; postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_;
} }
@@ -762,15 +763,15 @@ async function post(ev?: MouseEvent) {
let token = undefined; let token = undefined;
if (postAccount) { if (postAccount.value) {
const storedAccounts = await getAccounts(); const storedAccounts = await getAccounts();
token = storedAccounts.find(x => x.id === postAccount.id)?.token; token = storedAccounts.find(x => x.id === postAccount.value.id)?.token;
} }
posting = true; posting.value = true;
os.api('notes/create', postData, token).then(() => { os.api('notes/create', postData, token).then(() => {
if (props.freezeAfterPosted) { if (props.freezeAfterPosted) {
posted = true; posted.value = true;
} else { } else {
clear(); clear();
} }
@@ -782,8 +783,8 @@ async function post(ev?: MouseEvent) {
const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[]; const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[];
miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history)))); miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
} }
posting = false; posting.value = false;
postAccount = null; postAccount.value = null;
incNotesCount(); incNotesCount();
if (notesCount === 1) { if (notesCount === 1) {
@@ -828,7 +829,7 @@ async function post(ev?: MouseEvent) {
} }
}); });
}).catch(err => { }).catch(err => {
posting = false; posting.value = false;
os.alert({ os.alert({
type: 'error', type: 'error',
text: err.message + '\n' + (err as any).id, text: err.message + '\n' + (err as any).id,
@@ -842,7 +843,7 @@ function cancel() {
function insertMention() { function insertMention() {
os.selectUser().then(user => { os.selectUser().then(user => {
insertTextAtCursor(textareaEl, '@' + Misskey.acct.toString(user) + ' '); insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' ');
}); });
} }
@@ -852,7 +853,7 @@ async function insertEmoji(ev: MouseEvent) {
emojiPicker.show( emojiPicker.show(
ev.currentTarget ?? ev.target, ev.currentTarget ?? ev.target,
emoji => { emoji => {
insertTextAtCursor(textareaEl, emoji); insertTextAtCursor(textareaEl.value, emoji);
}, },
() => { () => {
textAreaReadOnly.value = false; textAreaReadOnly.value = false;
@@ -866,17 +867,17 @@ function showActions(ev) {
text: action.title, text: action.title,
action: () => { action: () => {
action.handler({ action.handler({
text: text, text: text.value,
cw: cw, cw: cw.value,
}, (key, value) => { }, (key, value) => {
if (key === 'text') { text = value; } if (key === 'text') { text.value = value; }
if (key === 'cw') { useCw = value !== null; cw = value; } if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
}); });
}, },
})), ev.currentTarget ?? ev.target); })), ev.currentTarget ?? ev.target);
} }
let postAccount = $ref<Misskey.entities.UserDetailed | null>(null); const postAccount = ref<Misskey.entities.UserDetailed | null>(null);
function openAccountMenu(ev: MouseEvent) { function openAccountMenu(ev: MouseEvent) {
if (props.mock) return; if (props.mock) return;
@@ -884,12 +885,12 @@ function openAccountMenu(ev: MouseEvent) {
openAccountMenu_({ openAccountMenu_({
withExtraOperation: false, withExtraOperation: false,
includeCurrentAccount: true, includeCurrentAccount: true,
active: postAccount != null ? postAccount.id : $i.id, active: postAccount.value != null ? postAccount.value.id : $i.id,
onChoose: (account) => { onChoose: (account) => {
if (account.id === $i.id) { if (account.id === $i.id) {
postAccount = null; postAccount.value = null;
} else { } else {
postAccount = account; postAccount.value = account;
} }
}, },
}, ev); }, ev);
@@ -905,23 +906,23 @@ onMounted(() => {
} }
// TODO: detach when unmount // TODO: detach when unmount
new Autocomplete(textareaEl, $$(text)); new Autocomplete(textareaEl.value, text);
new Autocomplete(cwInputEl, $$(cw)); new Autocomplete(cwInputEl.value, cw);
new Autocomplete(hashtagsInputEl, $$(hashtags)); new Autocomplete(hashtagsInputEl.value, hashtags);
nextTick(() => { nextTick(() => {
// 書きかけの投稿を復元 // 書きかけの投稿を復元
if (!props.instant && !props.mention && !props.specified && !props.mock) { if (!props.instant && !props.mention && !props.specified && !props.mock) {
const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey]; const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey.value];
if (draft) { if (draft) {
text = draft.data.text; text.value = draft.data.text;
useCw = draft.data.useCw; useCw.value = draft.data.useCw;
cw = draft.data.cw; cw.value = draft.data.cw;
visibility = draft.data.visibility; visibility.value = draft.data.visibility;
localOnly = draft.data.localOnly; localOnly.value = draft.data.localOnly;
files = (draft.data.files || []).filter(draftFile => draftFile); files.value = (draft.data.files || []).filter(draftFile => draftFile);
if (draft.data.poll) { if (draft.data.poll) {
poll = draft.data.poll; poll.value = draft.data.poll;
} }
} }
} }
@@ -929,21 +930,21 @@ onMounted(() => {
// 削除して編集 // 削除して編集
if (props.initialNote) { if (props.initialNote) {
const init = props.initialNote; const init = props.initialNote;
text = init.text ? init.text : ''; text.value = init.text ? init.text : '';
files = init.files; files.value = init.files;
cw = init.cw; cw.value = init.cw;
useCw = init.cw != null; useCw.value = init.cw != null;
if (init.poll) { if (init.poll) {
poll = { poll.value = {
choices: init.poll.choices.map(x => x.text), choices: init.poll.choices.map(x => x.text),
multiple: init.poll.multiple, multiple: init.poll.multiple,
expiresAt: init.poll.expiresAt, expiresAt: init.poll.expiresAt,
expiredAfter: init.poll.expiredAfter, expiredAfter: init.poll.expiredAfter,
}; };
} }
visibility = init.visibility; visibility.value = init.visibility;
localOnly = init.localOnly; localOnly.value = init.localOnly;
quoteId = init.renote ? init.renote.id : null; quoteId.value = init.renote ? init.renote.id : null;
} }
nextTick(() => watchForDraft()); nextTick(() => watchForDraft());

View File

@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { shallowRef } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import MkPostForm from '@/components/MkPostForm.vue'; import MkPostForm from '@/components/MkPostForm.vue';
@@ -22,6 +22,7 @@ const props = defineProps<{
mention?: Misskey.entities.User; mention?: Misskey.entities.User;
specified?: Misskey.entities.User; specified?: Misskey.entities.User;
initialText?: string; initialText?: string;
initialCw?: string;
initialVisibility?: typeof Misskey.noteVisibilities; initialVisibility?: typeof Misskey.noteVisibilities;
initialFiles?: Misskey.entities.DriveFile[]; initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean; initialLocalOnly?: boolean;
@@ -36,11 +37,11 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
let modal = $shallowRef<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
let form = $shallowRef<InstanceType<typeof MkPostForm>>(); const form = shallowRef<InstanceType<typeof MkPostForm>>();
function onPosted() { function onPosted() {
modal.close({ modal.value.close({
useSendAnimation: true, useSendAnimation: true,
}); });
} }

View File

@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, watch } from 'vue'; import { onMounted, onUnmounted, watch, ref, shallowRef } from 'vue';
import { deviceKind } from '@/scripts/device-kind.js'; import { deviceKind } from '@/scripts/device-kind.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { getScrollContainer } from '@/scripts/scroll.js'; import { getScrollContainer } from '@/scripts/scroll.js';
@@ -35,15 +35,15 @@ const RELEASE_TRANSITION_DURATION = 200;
const PULL_BRAKE_BASE = 1.5; const PULL_BRAKE_BASE = 1.5;
const PULL_BRAKE_FACTOR = 170; const PULL_BRAKE_FACTOR = 170;
let isPullStart = $ref(false); const isPullStart = ref(false);
let isPullEnd = $ref(false); const isPullEnd = ref(false);
let isRefreshing = $ref(false); const isRefreshing = ref(false);
let pullDistance = $ref(0); const pullDistance = ref(0);
let supportPointerDesktop = false; let supportPointerDesktop = false;
let startScreenY: number | null = null; let startScreenY: number | null = null;
const rootEl = $shallowRef<HTMLDivElement>(); const rootEl = shallowRef<HTMLDivElement>();
let scrollEl: HTMLElement | null = null; let scrollEl: HTMLElement | null = null;
let disabled = false; let disabled = false;
@@ -66,17 +66,17 @@ function getScreenY(event) {
} }
function moveStart(event) { function moveStart(event) {
if (!isPullStart && !isRefreshing && !disabled) { if (!isPullStart.value && !isRefreshing.value && !disabled) {
isPullStart = true; isPullStart.value = true;
startScreenY = getScreenY(event); startScreenY = getScreenY(event);
pullDistance = 0; pullDistance.value = 0;
} }
} }
function moveBySystem(to: number): Promise<void> { function moveBySystem(to: number): Promise<void> {
return new Promise(r => { return new Promise(r => {
const startHeight = pullDistance; const startHeight = pullDistance.value;
const overHeight = pullDistance - to; const overHeight = pullDistance.value - to;
if (overHeight < 1) { if (overHeight < 1) {
r(); r();
return; return;
@@ -85,36 +85,36 @@ function moveBySystem(to: number): Promise<void> {
let intervalId = setInterval(() => { let intervalId = setInterval(() => {
const time = Date.now() - startTime; const time = Date.now() - startTime;
if (time > RELEASE_TRANSITION_DURATION) { if (time > RELEASE_TRANSITION_DURATION) {
pullDistance = to; pullDistance.value = to;
clearInterval(intervalId); clearInterval(intervalId);
r(); r();
return; return;
} }
const nextHeight = startHeight - (overHeight / RELEASE_TRANSITION_DURATION) * time; const nextHeight = startHeight - (overHeight / RELEASE_TRANSITION_DURATION) * time;
if (pullDistance < nextHeight) return; if (pullDistance.value < nextHeight) return;
pullDistance = nextHeight; pullDistance.value = nextHeight;
}, 1); }, 1);
}); });
} }
async function fixOverContent() { async function fixOverContent() {
if (pullDistance > FIRE_THRESHOLD) { if (pullDistance.value > FIRE_THRESHOLD) {
await moveBySystem(FIRE_THRESHOLD); await moveBySystem(FIRE_THRESHOLD);
} }
} }
async function closeContent() { async function closeContent() {
if (pullDistance > 0) { if (pullDistance.value > 0) {
await moveBySystem(0); await moveBySystem(0);
} }
} }
function moveEnd() { function moveEnd() {
if (isPullStart && !isRefreshing) { if (isPullStart.value && !isRefreshing.value) {
startScreenY = null; startScreenY = null;
if (isPullEnd) { if (isPullEnd.value) {
isPullEnd = false; isPullEnd.value = false;
isRefreshing = true; isRefreshing.value = true;
fixOverContent().then(() => { fixOverContent().then(() => {
emit('refresh'); emit('refresh');
props.refresher().then(() => { props.refresher().then(() => {
@@ -122,17 +122,17 @@ function moveEnd() {
}); });
}); });
} else { } else {
closeContent().then(() => isPullStart = false); closeContent().then(() => isPullStart.value = false);
} }
} }
} }
function moving(event: TouchEvent | PointerEvent) { function moving(event: TouchEvent | PointerEvent) {
if (!isPullStart || isRefreshing || disabled) return; if (!isPullStart.value || isRefreshing.value || disabled) return;
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) { if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value)) {
pullDistance = 0; pullDistance.value = 0;
isPullEnd = false; isPullEnd.value = false;
moveEnd(); moveEnd();
return; return;
} }
@@ -143,13 +143,13 @@ function moving(event: TouchEvent | PointerEvent) {
const moveScreenY = getScreenY(event); const moveScreenY = getScreenY(event);
const moveHeight = moveScreenY - startScreenY!; const moveHeight = moveScreenY - startScreenY!;
pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE); pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
if (pullDistance > 0) { if (pullDistance.value > 0) {
if (event.cancelable) event.preventDefault(); if (event.cancelable) event.preventDefault();
} }
isPullEnd = pullDistance >= FIRE_THRESHOLD; isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
} }
/** /**
@@ -159,8 +159,8 @@ function moving(event: TouchEvent | PointerEvent) {
*/ */
function refreshFinished() { function refreshFinished() {
closeContent().then(() => { closeContent().then(() => {
isPullStart = false; isPullStart.value = false;
isRefreshing = false; isRefreshing.value = false;
}); });
} }
@@ -182,26 +182,26 @@ function onScrollContainerScroll() {
} }
function registerEventListenersForReadyToPull() { function registerEventListenersForReadyToPull() {
if (rootEl == null) return; if (rootEl.value == null) return;
rootEl.addEventListener('touchstart', moveStart, { passive: true }); rootEl.value.addEventListener('touchstart', moveStart, { passive: true });
rootEl.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない rootEl.value.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
} }
function unregisterEventListenersForReadyToPull() { function unregisterEventListenersForReadyToPull() {
if (rootEl == null) return; if (rootEl.value == null) return;
rootEl.removeEventListener('touchstart', moveStart); rootEl.value.removeEventListener('touchstart', moveStart);
rootEl.removeEventListener('touchmove', moving); rootEl.value.removeEventListener('touchmove', moving);
} }
onMounted(() => { onMounted(() => {
if (rootEl == null) return; if (rootEl.value == null) return;
scrollEl = getScrollContainer(rootEl); scrollEl = getScrollContainer(rootEl.value);
if (scrollEl == null) return; if (scrollEl == null) return;
scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true }); scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true });
rootEl.addEventListener('touchend', moveEnd, { passive: true }); rootEl.value.addEventListener('touchend', moveEnd, { passive: true });
registerEventListenersForReadyToPull(); registerEventListenersForReadyToPull();
}); });

View File

@@ -41,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue';
import { $i, getAccounts } from '@/account.js'; import { $i, getAccounts } from '@/account.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
@@ -62,26 +63,26 @@ defineProps<{
}>(); }>();
// ServiceWorker registration // ServiceWorker registration
let registration = $ref<ServiceWorkerRegistration | undefined>(); const registration = ref<ServiceWorkerRegistration | undefined>();
// If this browser supports push notification // If this browser supports push notification
let supported = $ref(false); const supported = ref(false);
// If this browser has already subscribed to push notification // If this browser has already subscribed to push notification
let pushSubscription = $ref<PushSubscription | null>(null); const pushSubscription = ref<PushSubscription | null>(null);
let pushRegistrationInServer = $ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>(); const pushRegistrationInServer = ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>();
function subscribe() { function subscribe() {
if (!registration || !supported || !instance.swPublickey) return; if (!registration.value || !supported.value || !instance.swPublickey) return;
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
return promiseDialog(registration.pushManager.subscribe({ return promiseDialog(registration.value.pushManager.subscribe({
userVisibleOnly: true, userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey), applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
}) })
.then(async subscription => { .then(async subscription => {
pushSubscription = subscription; pushSubscription.value = subscription;
// Register // Register
pushRegistrationInServer = await api('sw/register', { pushRegistrationInServer.value = await api('sw/register', {
endpoint: subscription.endpoint, endpoint: subscription.endpoint,
auth: encode(subscription.getKey('auth')), auth: encode(subscription.getKey('auth')),
publickey: encode(subscription.getKey('p256dh')), publickey: encode(subscription.getKey('p256dh')),
@@ -102,12 +103,12 @@ function subscribe() {
} }
async function unsubscribe() { async function unsubscribe() {
if (!pushSubscription) return; if (!pushSubscription.value) return;
const endpoint = pushSubscription.endpoint; const endpoint = pushSubscription.value.endpoint;
const accounts = await getAccounts(); const accounts = await getAccounts();
pushRegistrationInServer = undefined; pushRegistrationInServer.value = undefined;
if ($i && accounts.length >= 2) { if ($i && accounts.length >= 2) {
apiWithDialog('sw/unregister', { apiWithDialog('sw/unregister', {
@@ -115,11 +116,11 @@ async function unsubscribe() {
endpoint, endpoint,
}); });
} else { } else {
pushSubscription.unsubscribe(); pushSubscription.value.unsubscribe();
apiWithDialog('sw/unregister', { apiWithDialog('sw/unregister', {
endpoint, endpoint,
}); });
pushSubscription = null; pushSubscription.value = null;
} }
} }
@@ -150,20 +151,20 @@ if (navigator.serviceWorker == null) {
// TODO: よしなに? // TODO: よしなに?
} else { } else {
navigator.serviceWorker.ready.then(async swr => { navigator.serviceWorker.ready.then(async swr => {
registration = swr; registration.value = swr;
pushSubscription = await registration.pushManager.getSubscription(); pushSubscription.value = await registration.value.pushManager.getSubscription();
if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) { if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) {
supported = true; supported.value = true;
if (pushSubscription) { if (pushSubscription.value) {
const res = await api('sw/show-registration', { const res = await api('sw/show-registration', {
endpoint: pushSubscription.endpoint, endpoint: pushSubscription.value.endpoint,
}); });
if (res) { if (res) {
pushRegistrationInServer = res; pushRegistrationInServer.value = res;
} }
} }
} }
@@ -171,6 +172,6 @@ if (navigator.serviceWorker == null) {
} }
defineExpose({ defineExpose({
pushRegistrationInServer: $$(pushRegistrationInServer), pushRegistrationInServer: pushRegistrationInServer,
}); });
</script> </script>

View File

@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { computed } from 'vue';
const props = defineProps<{ const props = defineProps<{
modelValue: any; modelValue: any;
@@ -36,7 +36,7 @@ const emit = defineEmits<{
(ev: 'update:modelValue', value: any): void; (ev: 'update:modelValue', value: any): void;
}>(); }>();
let checked = $computed(() => props.modelValue === props.value); const checked = computed(() => props.modelValue === props.value);
function toggle(): void { function toggle(): void {
if (props.disabled) return; if (props.disabled) return;

View File

@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted, ref } from 'vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
@@ -27,13 +27,13 @@ const emit = defineEmits<{
(ev: 'end'): void; (ev: 'end'): void;
}>(); }>();
let up = $ref(false); const up = ref(false);
const zIndex = os.claimZIndex('middle'); const zIndex = os.claimZIndex('middle');
const angle = (90 - (Math.random() * 180)) + 'deg'; const angle = (90 - (Math.random() * 180)) + 'deg';
onMounted(() => { onMounted(() => {
window.setTimeout(() => { window.setTimeout(() => {
up = true; up.value = true;
}, 10); }, 10);
window.setTimeout(() => { window.setTimeout(() => {

View File

@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { inject, watch } from 'vue'; import { inject, watch, ref } from 'vue';
import XReaction from '@/components/MkReactionsViewer.reaction.vue'; import XReaction from '@/components/MkReactionsViewer.reaction.vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@@ -38,31 +38,31 @@ const emit = defineEmits<{
const initialReactions = new Set(Object.keys(props.note.reactions)); const initialReactions = new Set(Object.keys(props.note.reactions));
let reactions = $ref<[string, number][]>([]); const reactions = ref<[string, number][]>([]);
let hasMoreReactions = $ref(false); const hasMoreReactions = ref(false);
if (props.note.myReaction && !Object.keys(reactions).includes(props.note.myReaction)) { if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) {
reactions[props.note.myReaction] = props.note.reactions[props.note.myReaction]; reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction];
} }
function onMockToggleReaction(emoji: string, count: number) { function onMockToggleReaction(emoji: string, count: number) {
if (!mock) return; if (!mock) return;
const i = reactions.findIndex((item) => item[0] === emoji); const i = reactions.value.findIndex((item) => item[0] === emoji);
if (i < 0) return; if (i < 0) return;
emit('mockUpdateMyReaction', emoji, (count - reactions[i][1])); emit('mockUpdateMyReaction', emoji, (count - reactions.value[i][1]));
} }
watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => { watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
let newReactions: [string, number][] = []; let newReactions: [string, number][] = [];
hasMoreReactions = Object.keys(newSource).length > maxNumber; hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
for (let i = 0; i < reactions.length; i++) { for (let i = 0; i < reactions.value.length; i++) {
const reaction = reactions[i][0]; const reaction = reactions.value[i][0];
if (reaction in newSource && newSource[reaction] !== 0) { if (reaction in newSource && newSource[reaction] !== 0) {
reactions[i][1] = newSource[reaction]; reactions.value[i][1] = newSource[reaction];
newReactions.push(reactions[i]); newReactions.push(reactions.value[i]);
} }
} }
@@ -80,7 +80,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]); newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
} }
reactions = newReactions; reactions.value = newReactions;
}, { immediate: true, deep: true }); }, { immediate: true, deep: true });
</script> </script>

View File

@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, nextTick } from 'vue'; import { onMounted, nextTick, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@@ -23,11 +23,11 @@ import { initChart } from '@/scripts/init-chart.js';
initChart(); initChart();
const rootEl = $shallowRef<HTMLDivElement>(null); const rootEl = shallowRef<HTMLDivElement>(null);
const chartEl = $shallowRef<HTMLCanvasElement>(null); const chartEl = shallowRef<HTMLCanvasElement>(null);
const now = new Date(); const now = new Date();
let chartInstance: Chart = null; let chartInstance: Chart = null;
let fetching = $ref(true); const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({ const { handler: externalTooltipHandler } = useChartTooltip({
position: 'middle', position: 'middle',
@@ -38,8 +38,8 @@ async function renderChart() {
chartInstance.destroy(); chartInstance.destroy();
} }
const wide = rootEl.offsetWidth > 600; const wide = rootEl.value.offsetWidth > 600;
const narrow = rootEl.offsetWidth < 400; const narrow = rootEl.value.offsetWidth < 400;
const maxDays = wide ? 10 : narrow ? 5 : 7; const maxDays = wide ? 10 : narrow ? 5 : 7;
@@ -66,7 +66,7 @@ async function renderChart() {
} }
} }
fetching = false; fetching.value = false;
await nextTick(); await nextTick();
@@ -83,7 +83,7 @@ async function renderChart() {
const marginEachCell = 12; const marginEachCell = 12;
chartInstance = new Chart(chartEl, { chartInstance = new Chart(chartEl.value, {
type: 'matrix', type: 'matrix',
data: { data: {
datasets: [{ datasets: [{

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