Compare commits

...

76 Commits

Author SHA1 Message Date
syuilo
407a965c1d Merge pull request #10932 from misskey-dev/develop
Release: 13.13.0
2023-06-05 19:47:08 +09:00
syuilo
259be258aa Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-06-05 18:06:44 +09:00
Yuriha
2d5bb40ad0 Condensedlines reflow once (#10944)
* perf: Update MkCondensedLine styles after reading all dimensions

* perf: reduce reflow in MkCondensedLine

* lint

* Update packages/frontend/src/components/global/MkCondensedLine.vue

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Update packages/frontend/src/components/global/MkCondensedLine.vue

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
2023-06-05 18:06:33 +09:00
syuilo
db7fb1c688 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-06-05 18:05:32 +09:00
syuilo
6b0685a25c [ci skip] New Crowdin updates (#10947)
* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Chinese Traditional)
2023-06-05 18:05:20 +09:00
syuilo
618d07158a 13.13.0 2023-06-05 18:05:03 +09:00
syuilo
a63a417fd4 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-06-05 18:04:54 +09:00
syuilo
4443dba717 Update basic.cy.js 2023-06-05 18:04:51 +09:00
tamaina
565c502bbf fix(backend): pageのピン留めを解除することができない問題を修正
Fix #10950
2023-06-05 09:04:30 +00:00
tamaina
9e716fd813 fix(frontend): MkUserPopupが省略されないのを修正
Fix #10870
2023-06-05 08:58:00 +00:00
syuilo
34e1b52b38 Update e2e.js 2023-06-05 17:45:24 +09:00
syuilo
0cb3c7481c fix typo: schema -> scheme 2023-06-05 10:55:18 +09:00
syuilo
db623dda22 New Crowdin updates (#10943)
* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)
2023-06-04 14:31:03 +09:00
syuilo
1177528c25 fix backend e2e 2023-06-04 14:03:46 +09:00
mappi
a7abf941c0 fix:vue-plyr廃止追加対応(Audio要素のコンテキストメニューをブラウザデフォルトに) (#10940)
* wip

* add comment

* fix quotes
2023-06-02 16:31:25 +09:00
syuilo
5230ec883e fix(backend): 7日経過して無効化されたアンテナを再度有効化する方法がない問題を修正
Fix #10476
2023-06-02 15:18:34 +09:00
syuilo
482b00df77 🎨 2023-06-02 14:13:36 +09:00
syuilo
91d790bbb6 update deps 2023-06-02 11:34:38 +09:00
syuilo
40295ae57d fix style
Fix #10870
2023-06-02 11:03:59 +09:00
syuilo
3857cd589f New Crowdin updates (#10935)
* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Chinese Simplified)
2023-06-02 09:13:51 +09:00
syuilo
9eaca966a4 perf(backend): terminate stalled websocket connections
Resolve #10885
2023-06-02 09:13:41 +09:00
Acid Chicken (硫酸鶏)
8bdf0dd135 test: ignore MkImgWithBlurhash to avoid unstable snapshots 2023-06-01 23:27:58 +09:00
tamaina
eedc8049db fix(frontend): make scrollbar-color track transparent
Fix #9414
2023-06-01 12:03:07 +00:00
syuilo
23f272cc7d [ci skip] update patrons 2023-06-01 19:55:33 +09:00
syuilo
15450b18e8 [ci skip] make enableCondensedLineForAcct false by default
for performance
2023-06-01 19:50:51 +09:00
syuilo
50aeaf7498 [ci skip] 🎨 2023-06-01 19:45:06 +09:00
syuilo
861bfa06a8 🎨 2023-06-01 18:07:30 +09:00
Acid Chicken (硫酸鶏)
3b30ee3130 test: fix vitest 2023-06-01 08:50:26 +00:00
syuilo
f6830885d7 tweak of cd8274888 2023-06-01 17:34:56 +09:00
syuilo
ff56511638 13.13.0-beta.7 2023-06-01 17:20:25 +09:00
Acid Chicken (硫酸鶏)
337dd97b49 perf(#10923): CSS Modules のクラス名をインライン化する (#10930)
* perf(#10923): unwind css module class name

* perf(#10923): support multiple components

* refactor: clean up

* refactor(#10923): avoid `useCssModule()`

* fix(#10923): allow direct literal class name

* fix(#10923): avoid computed class name

* fix(#10923): allow literal keys

* fix(#10923): typo

* fix(#10923): invalid class names

* chore: test

* revert: test

This reverts commit 5c7ef366ec.

* fix(#10923): hidden tale

* perf(#10923): also unwind scoped css contained components

* perf(#10923): `normalizeClass` AOT compilation

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-06-01 17:19:46 +09:00
syuilo
146e89edd0 Update index.d.ts 2023-06-01 17:19:11 +09:00
syuilo
cd82748889 enhance(frontend): 追加の絵文字用辞書をダウンロードできるように
Resolve #10921
2023-06-01 17:10:53 +09:00
syuilo
2c0b10b0ee refactor 2023-06-01 16:28:24 +09:00
syuilo
6c09361ec6 🎨 2023-06-01 13:50:13 +09:00
SASAGAWA Kiyoshi
3089a86c8e fix: テーマにプロパティ 'fgOnWhite' を追加してフォローボタンのスタイルを調整 (#10931)
* fix: add theme property 'fgOnWhite' and fix styles of follow button.

* fix: add theme property 'fgOnWhite' and fix styles of follow button.
2023-06-01 13:29:44 +09:00
syuilo
ec2f05d4f7 fix(backend): i/notificationsのsinceIdが機能しない問題を修正
Fix #10902
2023-06-01 13:28:43 +09:00
syuilo
31a8129cb9 New translations ja-JP.yml (Korean) (#10933) 2023-06-01 13:20:43 +09:00
syuilo
8cc6c2c864 New Crowdin updates (#10929)
* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Norwegian)
2023-06-01 09:29:29 +09:00
syuilo
a4de927df8 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-06-01 09:20:39 +09:00
syuilo
a2c77a0944 Update style.scss 2023-06-01 09:20:37 +09:00
syuilo
d8a564c6a0 🎨 2023-06-01 08:54:37 +09:00
syuilo
ff94b64c91 🎨 2023-06-01 07:51:02 +09:00
tamaina
1cc616b86c fix(frontend): disconnect ResizeObserver 2023-05-31 16:04:01 +00:00
syuilo
6addf9002c tweak ui 2023-05-31 18:03:43 +09:00
mappi
6dd219b6c7 fix: Firefoxにおける絵文字ピッカーのTabキーフォーカス問題の修正 (#10926)
* fix 10744

* fix 10744

* Update CHANGELOG.md

* add comment
2023-05-31 14:03:54 +09:00
syuilo
a535142e82 13.13.0-beta.6 2023-05-31 13:43:36 +09:00
syuilo
9521519cb8 reafactor 2023-05-31 13:41:38 +09:00
syuilo
e11f82c300 refactor 2023-05-31 13:27:59 +09:00
kabo2468
aba0755880 enhance(client): MFMのx2, scale, positionが含まれていたらノートをたたむようにした (#10165)
* enhance(client): MFMのx2, scale, positionが含まれていたらノートをたたむようにした

* Update CHANGELOG.md

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-05-31 12:57:40 +09:00
syuilo
821bb1c476 perf(frontend): サーバーにカスタム絵文字の種類が多い場合のパフォーマンスの改善
Resolve #10925
2023-05-31 12:42:24 +09:00
syuilo
14da0a65f7 tweak ui 2023-05-31 12:24:00 +09:00
syuilo
8e5d31eb5c New Crowdin updates (#10918)
* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Norwegian)
2023-05-31 09:06:29 +09:00
tamaina
f8f3304164 fix(frontend/MkNoteDetailed): fix css module 2023-05-30 16:24:45 +00:00
syuilo
d7efdd7123 perf(frontend): minify file names
Resolve #10924
2023-05-30 18:55:22 +09:00
syuilo
de6348e8a0 Merge pull request #10833 from misskey-dev/develop
* refactor(frontend): use css modules

* feat: 投稿したコンテンツのAIによる学習を軽減するオプションを追加

Resolve #10819

* enhance(backend): publicReactionsをデフォルトtrueに

* 念のためnoimageaiもつける

* add X-Robots-Tag: noai

* Update ja-JP.yml

* fix(frontend): ブラーエフェクトを有効にしている状態で高負荷になる問題を修正

* enhance(backend): graceful shutdown for job queue and refactor

* fix(backend): テスト時は一部のサービスを停止

* fix test

* New Crowdin updates (#10815)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* refactor

* bump

* refactor(frontend): use css module

* refactor(frontend): use css module

* delete unused component

* センシティブワードを正規表現、CWにも適用するように (#10688)

* cwにセンシティブが効いてない

* CWが無いときにTextを見るように

* 比較演算子間違えた

* とりあえずチェック

* 正規表現対応

* /test/giにも対応

* matchでしなくてもいいのでは感

* レビュー修正

* Update packages/backend/src/core/NoteCreateService.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Update packages/backend/src/core/NoteCreateService.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* 修正

* wipかも

* wordsでスペース区切りのものできたかも

* なんか動いたかも

* test作成

* 文言の修正

* 修正

* note参照

---------

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Update CHANGELOG.md

* New Crowdin updates (#10823)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* ci: fix typo

* fix(frontend): より明確な説明にしたのとtypo修正

* fix typo

* fix(frontend): カラーバーがリプライには表示されないのを修正

* fix(frontend): チャンネル内の検索ボックスが挙動不審な問題を修正

Fix #10793

* enhance(backend): ノートのハッシュタグもMeilisearchに突っ込むように

今後ハッシュタグ検索とか実装するときのため

* feat(frontend): ユーザー指定ノート検索

* fix(frontend): fix retention chart rendering

* Update about-misskey.vue

* meta: Remove @rinsuki from reviewer-lottery (#10830)

* New Crowdin updates (#10824)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Thai)

* enhance(frontend): アカウント初期設定ウィザードにプライバシー設定を追加

* Update CHANGELOG.md

* fix(backend): ひとつのMeilisearchサーバーを複数のMisskeyサーバーで使えない問題を修正

* fix MkUserSetupDialog.Privacy.vue

* ci: skip non-Japanese locale on TurboSnap

* ci: notify on changes for push events

* ci: fix missing branch

* Update basic.cy.js

* [ci skip] New Crowdin updates (#10834)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Arabic)

* 🎨

* 🎨

* enhance(frontend): add retention line chart

* update deps

* refactor

* fix(frontend): Pageにおいて画像ブロックに画像を設定できない問題を修正

Fix #10837

---------

Co-authored-by: nenohi <kimutipartylove@gmail.com>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Co-authored-by: rinsuki <428rinsuki+git@gmail.com>
2023-05-12 12:41:53 +09:00
syuilo
9ad57324db Merge pull request #10814 from misskey-dev/develop
Release: 13.12.1
2023-05-09 15:38:17 +09:00
syuilo
94690c835e Merge pull request #10774 from misskey-dev/develop
Release: 13.12.0
2023-05-09 09:17:34 +09:00
syuilo
c5d2dba28d Merge pull request #10608 from misskey-dev/develop
Release: 13.11.3
2023-04-13 12:18:07 +09:00
syuilo
272e0c874f Merge pull request #10606 from misskey-dev/EbiseLutica-patch-1
Update CHANGELOG.md
2023-04-13 08:35:14 +09:00
Ebise Lutica
d429f810a9 Update CHANGELOG.md 2023-04-13 00:31:22 +09:00
syuilo
75b28d6782 Merge pull request #10578 from misskey-dev/develop
Release: 13.11.2
2023-04-11 15:51:07 +09:00
syuilo
8b1362ab03 Merge pull request #10543 from misskey-dev/develop
Release: 13.11.1
2023-04-09 10:29:36 +09:00
syuilo
a096f621cf Merge pull request #10506 from misskey-dev/develop
13.11.0
2023-04-08 21:27:21 +09:00
syuilo
f54a9542bb Merge pull request #10402 from misskey-dev/develop
Release: 13.10.3
2023-03-25 08:36:41 +09:00
syuilo
a52bbc7c8d Merge pull request #10388 from misskey-dev/develop
Release: 13.10.2
2023-03-22 18:47:10 +09:00
syuilo
59768bdf3f Merge pull request #10383 from misskey-dev/develop
Release: 13.10.1
2023-03-22 16:30:36 +09:00
syuilo
1e67e9c661 Merge pull request #10342 from misskey-dev/develop
Release: 13.10.0
2023-03-22 09:55:38 +09:00
syuilo
ae517a99a7 Merge pull request #10218 from misskey-dev/develop
Release: 13.9.2
2023-03-06 11:54:12 +09:00
syuilo
b23a9b1a88 Merge pull request #10181 from misskey-dev/develop
Release: 13.9.1
2023-03-03 20:56:50 +09:00
syuilo
5bd68aa3e0 Merge pull request #10177 from misskey-dev/develop
Release: 13.9.0
2023-03-03 15:35:40 +09:00
syuilo
647ce174b3 Merge pull request #10112 from misskey-dev/develop
Release: 13.8.1
2023-02-26 20:57:13 +09:00
syuilo
02c8fd9de5 Merge pull request #10108 from misskey-dev/develop
* Add dialog to remove follower (#9718)

* update PULL_REQUEST_TEMPLATE

* 起動時にRedisの疎通確認を行う (#9832)

* 起動時にRedisの疎通確認を行う

* check:connectをstart内に移動

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* Pass `--detectOpenHandles` to Jest (#9895)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* enhance(client): MkUrlPreviewの閉じるボタンを見やすく (#9913)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* test(backend): restore ap-request tests (#9997)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* fix/refaftor(client): MkTime.vueの変更 (#10061)

* fix(client): MkTime.timeにstringでもDateでない値が入った場合、?を表示

* fix(client): MkTimeを改良

* numberを許容

* falsyな値もとる

* 不明

* ありません

* fix

* fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする (#9911)

* fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする

* NO_SUCH_FILE

* Update codecov.yml

* Update apple-touch-icon.png

* デプロイされているプレビュー環境がない場合はプレビュー環境を削除しないようにする (#10062)

* デプロイされているプレビュー環境がない場合はDestroy preview environmentを実行しないようにする

* CIがない場合の処理追加

* enhance(client): improve clip menu ux

* 未知のユーザーが deleteActor されたら処理をスキップする (#10067)

* fix(client): Android ChromeでPWAとしてインストールできない問題を修正 (#10069)

* fix(client): Android ChromeでPWAとしてインストールできない問題を修正

* 順番関係ある?

* Windows環境でswcを使うと正常にビルドができない問題の修正 (#10074)

* Update @swc/core to v1.3.36

* Update CHANGELOG.md

* Update CHANGELOG.md

* バックグラウンドで一定時間経過したらページネーションのアイテム更新をしない (#10053)

* 🎨

* feat: 2つの検索画面の統合 (#9949) (#10038)

* feat: 検索画面の UI を統一

* fix: エラーの修正

* add: changelog

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* enhance(client): ノートメニューからユーザーメニューを開けるように

Resolve #10019

* enhance(client): renoteした際の表示を改善

Resolve #10078

* Update CHANGELOG.md

* enhance(client): tweak contextmenu position calculation

* 🎨

* 🎨

* feat: in-channel featured note

Resolve #9938

* refactor(frontend): fix eslint error (#10084)

* Simplify search.vue (remove dead code) (#10088)

* Simplify search.vue

This is already handled by the code above it, no need to handle it twice

* Remove unused imports

* Update about-misskey.vue

* test(server): add validation test of api:notes/create (#10090)

* fix(server): notes/createのバリデーションが効いていない
Fix #10079

Co-Authored-By: mei23 <m@m544.net>

* anyOf内にバリデーションを書いても最初の一つしかチェックされない

* ✌️

* wip

* wip

* ✌️

* RequiredProp

* Revert "RequiredProp"

This reverts commit 7469390011.

* add api:notes/create

* fix lint

* text

* ✌️

* improve readability

---------

Co-authored-by: mei23 <m@m544.net>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* New Crowdin updates (#10059)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Romanian)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Bengali)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* enhance(client): improve user menu ux

* enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように (#10098)

* enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように

* add: changelog

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* enhance(client): メニューの「もっと」からインスタンス情報を見れるように

* [Fix] fixed an typo in error message (#10102)

* Update codecov.yml

* Update CHANGELOG.md

* fix(server): エラーのスタックトレースは返さないように

Fix #10064

* [chore]Editorconfig: ymlに加えてyamlファイルに対しても同じ規約を適用する (#10081)

* Added yaml file in addition to yml file, in editorconfig

* Applied editorconfig for pnpm-workspace.yaml

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* update deps

* ホームタイムラインの読み込みでクエリタイムアウトになるのを修正する (#10106)

* refactor

* New translations ja-JP.yml (French) (#10103)

* Update CHANGELOG.md

* 13.8.0

---------

Co-authored-by: atsuchan <83960488+atsu1125@users.noreply.github.com>
Co-authored-by: Masaya Suzuki <15100604+massongit@users.noreply.github.com>
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com>
Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
Co-authored-by: xianon <xianon@hotmail.co.jp>
Co-authored-by: kabo2468 <28654659+kabo2468@users.noreply.github.com>
Co-authored-by: YS <47836716+yszkst@users.noreply.github.com>
Co-authored-by: Khsmty <me@khsmty.com>
Co-authored-by: Soni L <EnderMoneyMod@gmail.com>
Co-authored-by: mei23 <m@m544.net>
Co-authored-by: daima3629 <52790780+daima3629@users.noreply.github.com>
Co-authored-by: Windymelt <1113940+windymelt@users.noreply.github.com>
2023-02-26 20:21:54 +09:00
syuilo
1ba49b614d Merge pull request #10058 from misskey-dev/develop
Release: 13.7.5
2023-02-24 13:06:55 +09:00
tamaina
40de14415c Release: 13.7.4
Merge pull request #10050 from misskey-dev/develop
2023-02-23 23:11:25 +09:00
tamaina
7c9330a02f Release: 13.7.3
Merge pull request #10048 from misskey-dev/develop
2023-02-23 22:15:56 +09:00
121 changed files with 5839 additions and 1550 deletions

View File

@@ -32,16 +32,23 @@
- ハッシュタグのノート一覧ページから、そのハッシュタグで投稿するボタンを追加
- アカウント初期設定ウィザードに戻るボタンを追加
- アカウントの初期設定ウィザードにあとでボタンを追加
- サーバーにカスタム絵文字の種類が多い場合のパフォーマンスの改善
- Fix: URLプレビューで情報が取得できなかった際の挙動を修正
- Fix: Safari、Firefoxでの新規登録時、パスワードマネージャーにメールアドレスが登録されていた挙動を修正
- Fix: ロールタイムラインが無効でも投稿が流れてしまう問題の修正
- Fix: ロールタイムラインにて全ての投稿が流れてしまう問題の修正
- Fix: 「アクセストークンの管理」画面でアプリの情報が表示されない問題の修正
- Fix: Firefoxにおける絵文字ピッカーのTabキーフォーカス問題の修正
- Fix: フォローボタンがテーマのカラースキームによって視認性が悪くなる問題を修正
- 新しいプロパティ `fgOnWhite` が追加されました
### Server
- bullをbull-mqにアップグレードし、ジョブキューのパフォーマンスを改善
- ストリーミングのパフォーマンスを改善
- Fix: 無効化されたアンテナにアクセスがあった際に再度有効化するように
- Fix: お知らせの画像URLを空にできない問題を修正
- Fix: i/notificationsのsinceIdが機能しない問題を修正
- Fix: pageのピン留めを解除することができない問題を修正
## 13.12.2
@@ -366,6 +373,7 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
- アンテナでCWも検索対象にするように
- ノートの操作部をホバー時のみ表示するオプションを追加
- サウンドを追加
- enhance(client): MFMのx2, scale, positionが含まれていたらートをたたむように
- サーバーのパフォーマンスを改善
### Bugfixes

View File

@@ -169,25 +169,20 @@ describe('After user signed in', () => {
cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ');
// TODO: アイコン設定テスト
cy.get('[data-cy-user-setup-back]').click();
cy.get('[data-cy-user-setup-continue]').click();
// プライバシー設定
cy.get('[data-cy-user-setup-back]').click();
cy.get('[data-cy-user-setup-continue]').click();
// フォローはスキップ
cy.get('[data-cy-user-setup-back]').click();
cy.get('[data-cy-user-setup-continue]').click();
// プッシュ通知設定はスキップ
cy.get('[data-cy-user-setup-back]').click();
cy.get('[data-cy-user-setup-continue]').click();
cy.get('[data-cy-user-setup-back]').click();
cy.get('[data-cy-user-setup-continue]').click();
});
});

View File

@@ -21,6 +21,8 @@ import './commands'
Cypress.on('uncaught:exception', (err, runnable) => {
if ([
'The source image cannot be decoded',
// Chrome
'ResizeObserver loop limit exceeded',

View File

@@ -267,8 +267,8 @@ start: "البداية"
home: "الرئيسي"
remoteUserCaution: "هذه المعلومات قد لا تكون مكتملة بما أن المستخدم من مثيل بعيد."
activity: "النشاط"
images: "الصور"
image: "الصور"
images: "صور"
image: "صور"
birthday: "تاريخ الميلاد"
yearsOld: "{age} سنة"
registeredDate: "انضم في"
@@ -1331,7 +1331,7 @@ _pages:
text: "نص"
textarea: "حقل نصي"
section: "قسم"
image: "الصور"
image: "صور"
button: "زرّ"
note: "ملاحظة مضمّنة"
_note:

View File

@@ -1060,6 +1060,8 @@ cancelReactionConfirm: "Möchtest du deine Reaktion wirklich löschen?"
changeReactionConfirm: "Möchtest du deine Reaktion wirklich ändern?"
later: "Später"
goToMisskey: "Zu Misskey"
additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher"
installed: "Installiert"
_initialAccountSetting:
accountCreated: "Dein Konto wurde erfolgreich erstellt!"
letsStartAccountSetup: "Lass uns nun dein Konto einrichten."

View File

@@ -1060,6 +1060,8 @@ cancelReactionConfirm: "Really delete your reaction?"
changeReactionConfirm: "Really change your reaction?"
later: "Later"
goToMisskey: "To Misskey"
additionalEmojiDictionary: "Additional emoji dictionaries"
installed: "Installed"
_initialAccountSetting:
accountCreated: "Your account was successfully created!"
letsStartAccountSetup: "For starters, let's set up your profile."

2
locales/index.d.ts vendored
View File

@@ -1063,6 +1063,8 @@ export interface Locale {
"changeReactionConfirm": string;
"later": string;
"goToMisskey": string;
"additionalEmojiDictionary": string;
"installed": string;
"_initialAccountSetting": {
"accountCreated": string;
"letsStartAccountSetup": string;

View File

@@ -1060,6 +1060,8 @@ cancelReactionConfirm: "リアクションを取り消しますか?"
changeReactionConfirm: "リアクションを変更しますか?"
later: "あとで"
goToMisskey: "Misskeyへ"
additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み"
_initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!"

View File

@@ -47,11 +47,13 @@ copyContent: "内容をコピー"
copyLink: "リンクをコピー"
delete: "ほかす"
deleteAndEdit: "ほかして直す"
deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのリアクション、Renote、返信も全部消えるんやけどそれでもええん"
deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのツッコミ、Renote、返信も全部消えるんやけどそれでもええん"
addToList: "リストに入れたる"
sendMessage: "メッセージを送る"
copyRSS: "RSSをコピー"
copyUsername: "ユーザー名をコピー"
copyUserId: "ユーザーIDをコピー"
copyNoteId: "ートIDをコピー"
searchUser: "ユーザーを検索"
reply: "返事"
loadMore: "まだまだあるで!"
@@ -1043,6 +1045,10 @@ preventAiLearning: "生成AIの学習に使わんといて"
preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
options: "オプション"
specifyUser: "ユーザー指定"
rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールが一個も指定されてへんかったら、誰でもツッコミとして使えるで。"
cancelReactionConfirm: "ツッコむんをやっぱやめるか?"
changeReactionConfirm: "ツッコミを別のに変えるか?"
_initialAccountSetting:
accountCreated: "アカウント作り終わったで。"
letsStartAccountSetup: "アカウントの初期設定をしよか。"
@@ -1614,7 +1620,7 @@ _timelineTutorial:
step2_2: "最初のノートは、自己紹介とか「{name}始めてみたんや」とかがええと思うで。"
step3_1: "投稿できた?"
step3_2: "あんたのノートがタイムラインに出てきたら成功や。"
step4_1: "ノートには、「リアクション」を付けれるで。"
step4_1: "ノートには、「ツッコミ」を付けれるで。"
step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶで。"
_2fa:
alreadyRegistered: "もう設定終わっとるわ。"

View File

@@ -52,6 +52,8 @@ addToList: "리스트에 추가"
sendMessage: "메시지 보내기"
copyRSS: "RSS 복사"
copyUsername: "유저명 복사"
copyUserId: "유저 ID 복사"
copyNoteId: "노트 ID 복사"
searchUser: "사용자 검색"
reply: "답글"
loadMore: "더 보기"
@@ -505,7 +507,7 @@ objectStoragePrefixDesc: "이 Prefix 의 디렉토리 아래에 파일이 저장
objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "AWS S3의 경우 공란, 다른 서비스의 경우 각 서비스의 가이드에 맞게 endpoint를 설정해주세요. '<host>' 혹은 '<host>:<port>' 와 같이 지정합니다."
objectStorageRegion: "Region"
objectStorageRegionDesc: "'xx-east-1'와 같이 region을 지정해주세요. 사용하는 서비스에 region 개념이 없는 경우 'us-east-1'으로 설정해 주세요. AWS 설정 파일 또는 환경 변수를 참조할 경우에는 비워주세요."
objectStorageRegionDesc: "'xx-east-1'와 같이 region을 지정해 주세요. 사용하는 서비스에 region 개념이 없는 경우 'us-east-1'으로 설정해 주세요. AWS 설정 파일 또는 환경 변수를 참조할 경우에는 비워주세요."
objectStorageUseSSL: "SSL 사용"
objectStorageUseSSLDesc: "API 호출시 HTTPS 를 사용하지 않는 경우 OFF 로 설정해 주세요"
objectStorageUseProxy: "연결에 프록시를 사용"
@@ -790,6 +792,7 @@ noMaintainerInformationWarning: "관리자 정보가 설정되어 있지 않습
noBotProtectionWarning: "Bot 방어가 설정되어 있지 않습니다."
configure: "설정하기"
postToGallery: "갤러리에 업로드"
postToHashtag: "이 해시태그에 게시"
gallery: "갤러리"
recentPosts: "최근 포스트"
popularPosts: "인기 포스트"
@@ -823,6 +826,7 @@ translatedFrom: "{x}에서 번역"
accountDeletionInProgress: "계정 삭제 작업을 진행하고 있습니다"
usernameInfo: "서버상에서 계정을 식별하기 위한 이름. 알파벳(a~z, A~Z), 숫자(0~9) 및 언더바(_)를 사용할 수 있습니다. 사용자명은 나중에 변경할 수 없습니다."
aiChanMode: "아이 모드"
devMode: "개발자 모드"
keepCw: "CW 유지하기"
pubSub: "Pub/Sub 계정"
lastCommunication: "마지막 통신"
@@ -830,8 +834,10 @@ resolved: "해결됨"
unresolved: "해결되지 않음"
breakFollow: "팔로워 해제"
breakFollowConfirm: "팔로우를 해제하시겠습니까?"
itsOn: "켜"
itsOff: "꺼"
itsOn: "켜져 있습니다"
itsOff: "꺼져 있습니다"
on: "켜짐"
off: "꺼짐"
emailRequiredForSignup: "가입할 때 이메일 주소 입력을 필수로 하기"
unread: "읽지 않음"
filter: "필터"
@@ -864,7 +870,7 @@ instanceDefaultLightTheme: "서버 기본 라이트 테마"
instanceDefaultDarkTheme: "서버 기본 다크 테마"
instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요."
mutePeriod: "뮤트할 기간"
period: "투표 기한"
period: "기간"
indefinitely: "무기한"
tenMinutes: "10분"
oneHour: "1시간"
@@ -986,6 +992,8 @@ cannotBeChangedLater: "나중에 변경할 수 없습니다."
reactionAcceptance: "리액션 수신"
likeOnly: "좋아요만 받기"
likeOnlyForRemote: "리모트에서는 좋아요만 받기"
nonSensitiveOnly: "열람 주의로 설정되지 않았을 때만 받기"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "열람 주의로 설정되지 않았을 때만 받기 (리모트에서는 좋아요만 받기)"
rolesAssignedToMe: "나에게 할당된 역할"
resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?"
sensitiveWords: "민감한 단어"
@@ -1043,31 +1051,43 @@ preventAiLearning: "기계학습(생성형 AI)으로의 사용을 거부"
preventAiLearningDescription: "외부의 문장 생성 AI나 이미지 생성 AI에 대해 제출한 노트나 이미지 등의 콘텐츠를 학습의 대상으로 사용하지 않도록 요구합니다. 다만, 이 요구사항을 지킬 의무는 없기 때문에 학습을 완전히 방지하는 것은 아닙니다."
options: "옵션"
specifyUser: "사용자 지정"
failedToPreviewUrl: "미리 볼 수 없음"
update: "업데이트"
rolesThatCanBeUsedThisEmojiAsReaction: "이 이모지를 리액션으로 사용할 수 있는 역할"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "역할을 지정하지 않으면, 누구나 이 이모지를 리액션으로 사용할 수 있습니다."
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "역할은 공개로 설정되어 있어야 합니다."
cancelReactionConfirm: "리액션을 취소하시겠습니까?"
changeReactionConfirm: "리액션을 변경하시겠습니까?"
later: "나중에"
goToMisskey: "Misskey로"
additionalEmojiDictionary: "이모지 추가 사전"
installed: "설치됨"
_initialAccountSetting:
accountCreated: "계정 생성이 완료되었습니다!"
letsStartAccountSetup: "계정의 초기 설정을 진행합니다."
letsFillYourProfile: "우선 나의 프로필을 설정해 보아요."
profileSetting: "프로필 설정"
privacySetting: "\n프라이버시설정"
privacySetting: "프라이버시 설정"
theseSettingsCanEditLater: "이 설정들은 나중에도 변경할 수 있습니다."
youCanEditMoreSettingsInSettingsPageLater: "이 외에도 '설정' 페이지에서 다양한 설정을 나의 입맛에 게 조절할 수 있습니다. 꼭 확인해 보세요!"
youCanEditMoreSettingsInSettingsPageLater: "이 외에도 '설정' 페이지에서 다양한 설정을 나의 입맛에 게 조절할 수 있습니다. 꼭 확인해 보세요!"
followUsers: "관심사가 맞는 유저를 팔로우하여 타임라인을 가꾸어 봅시다."
pushNotificationDescription: "푸시 알림을 활성화하면 {name}의 알림을 나의 기기에서 받아볼 수 있게 됩니다."
initialAccountSettingCompleted: "초기 설정을 모두 마쳤습니다!"
haveFun: "{name}와 함께 즐거운 시간 보내세요!"
ifYouNeedLearnMore: "{name}(Misskey)의 사용 방법에 대해 자세히 알아보려면 {link}를 참고해 주세요."
skipAreYouSure: "초기 설정을 넘기시겠습니까?"
skipAreYouSure: "초기 설정을 중단하시겠습니까?"
laterAreYouSure: "초기 설정을 나중에 진행하시겠습니까?"
_serverRules:
description: "회원 가입 이전에 간단하게 표시할 서버 규칙입니다. 이용 약관의 요약으로 구성하는 것을 추천합니다."
_accountMigration:
moveFrom: "다른 계정에서 이 계정으로 이사"
moveFromSub: "다른 계정에 대한 별칭을 생성"
moveFromLabel: "기존 계정:"
moveFromLabel: "기존 계정 #{n}"
moveFromDescription: "다른 계정에서 이 계정으로 팔로워를 가져오려면, 우선 여기에서 별칭을 지정해야 합니다. 반드시 이사하기 전에 지정해야 합니다! 기존 계정을 다음과 같은 형식으로 입력해 주십시오: @person@instance.com"
moveTo: "이 계정에서 다른 계정으로 이사"
moveToLabel: "이사할 계정:"
moveCannotBeUndone: "한 번 이사하면, 두 번 다시 되돌릴 수 없습니다."
moveAccountDescription: "이 작업은 취소할 수 없습니다. 먼저 이사할 계정에서 이 계정에 대한 별칭을 지정하였는지 다시 한 번 확인해 주십시오. 별칭을 지정한 다음, 이사할 계정을 다음과 같은 형식으로 입력해 주십시오: @person@instance.com"
moveAccountDescription: "새 계정으로 이전합니다.\n ・팔로워가 새 계정을 자동으로 팔로우 합니다\n ・이 계정에서 팔로우는 모두 해제됩니다\n ・이 계정으로는 노트 작성 등을 할 수 없게 됩니다\n\n팔로워는 자동으로 이전되지만, 팔로우는 수동으로 진행해야 합니다. 이전하기 전에 이 계정에서 팔로우를 내보내고, 이전 후에는 즉시 이전한 계정에서 가져오기를 진행하십시오.\n리스트・뮤트・차단에 대해서도 마찬가지이므로 수동으로 이전해야 합니다.\n\n(이 설명은 이 서버(Misskey v13.12.0 이후)의 사양입니다. Mastodon 등의 다른 ActivityPub 소프트웨어에서는 작동이 다를 수 있습니다.)"
moveAccountHowTo: "계정을 이사하려면 우선 이사갈 계정에서 이 계정에 대한 별칭을 지정해야 합니다.\n별칭을 작성한 다음, 이사갈 계정을 다음과 같이 입력하십시오:\n@username@server.example.com"
startMigration: "이사하기"
migrationConfirm: "정말로 이 계정을 {account} 으로 이전하시겠습니까? 한 번 이전한 다음에는 취소할 수 없으며, 두 번 다시 원래 상태로 복구할 수 없습니다.\n이사할 계정에서 계정 별칭을 지정하였는지 다시 한 번 확인하십시오."

View File

@@ -1,5 +1,7 @@
---
_lang_: "Norsk Bokmål"
headlineMisskey: "Et nettverk forbundet med Notes"
introMisskey: "Velkommen! Misskey er en desentralisert mikrobloggtjeneste med åpen kildekode.\nOpprett \"Notes\" for å dele tankene dine med alle rundt deg. 📡\nMed \"reaksjoner\" kan du også raskt gi uttrykk for hva du synes om alles Notes. 👍\nLa oss utforske en ny verden! 🚀"
monthAndDay: "{day}-{month}"
search: "Søk"
notifications: "Varsler"
@@ -10,8 +12,10 @@ fetchingAsApObject: "Henter fra Fediverse..."
ok: "OK"
gotIt: "Skjønner"
cancel: "Avbryt"
noThankYou: "Avbryt"
noThankYou: "Ikke nå"
enterUsername: "Skriv inn brukernavn"
renotedBy: "Renotes av {user}"
noNotes: "Ingen Notes"
noNotifications: "Ingen varsler"
instance: "Server"
settings: "Innstillinger"
@@ -21,7 +25,7 @@ otherSettings: "Andre innstillinger"
openInWindow: "Åpne i vindu"
profile: "Profil"
timeline: "Tidslinje"
noAccountDescription: "Denne brukeren har ikke skrevet sin bio ennå."
noAccountDescription: "Denne brukeren har ikke skrevet sin biografi ennå."
login: "Logg inn"
loggingIn: "Logget inn"
logout: "Logg ut"
@@ -30,20 +34,21 @@ uploading: "Laster opp"
save: "Lagre"
users: "Brukere"
addUser: "Legg til bruker"
favorite: "Favoritt"
favorite: "Legg til i favoritter"
favorites: "Favoritter"
unfavorite: "Fjern favoritt"
unfavorite: "Fjern fra favoritter"
favorited: "Lagt til i favoritter."
alreadyFavorited: "Allerede lagt til i favoritter."
cantFavorite: "Kunne ikke legge til i favoritter."
pin: "Fest"
unpin: "Opphev festing"
pin: "Fest til profil"
unpin: "Fjern fra profil"
copyContent: "Kopier innhold"
copyLink: "Kopier lenke"
delete: "Slett"
deleteAndEdit: "Slett og rediger"
deleteAndEditConfirm: "Er du sikker på at du vil slette denne Noten og redigere den? Du vil miste alle reaksjoner, Renotes og svar på den."
addToList: "Legg til i liste"
sendMessage: "Send melding"
sendMessage: "Send en melding"
copyRSS: "Kopier RSS"
copyUsername: "Kopier brukernavn"
searchUser: "Søk brukere"
@@ -63,7 +68,9 @@ unfollowConfirm: "Er du sikker på at du vil slutte å følge {name}?"
importRequested: "Du har bedt om import. Dette kan ta en stund."
lists: "Lister"
noLists: "Ingen lister"
following: "Følg"
note: "Note"
notes: "Notes"
following: "Følger"
followers: "Følgere"
followsYou: "Følger deg"
createList: "Opprett liste"
@@ -74,14 +81,22 @@ pageLoadError: "Kunne ikke hente side."
serverIsDead: "Denne serveren svarer ikke. Vennligst vent en stund og prøv igjen."
enterListName: "Skriv inn et navn på listen"
privacy: "Personvern"
defaultNoteVisibility: "Standard synlighet"
follow: "Følg"
followRequest: "Følgeforespørsel"
followRequests: "Følgeforespørsel"
unfollow: "Avfølg"
followRequestPending: "Venter på godkjenning"
enterEmoji: "Skriv inn en emoji"
renote: "Renote"
renoted: "Renotet."
cantRenote: "Dette innlegget kan ikke renotes."
cantReRenote: "En Renote kan ikke renotes."
quote: "Sitat"
pinned: "Fest"
inChannelRenote: "Renote kun for kanal"
inChannelQuote: "Sitat kun for kanal"
pinnedNote: "Festet Note"
pinned: "Fest til profil"
you: "Du"
clickToShow: "Klikk for å vise"
add: "Legg til"
@@ -89,18 +104,21 @@ reaction: "Reaksjon"
reactions: "Reaksjoner"
reactionSetting: "Reaksjoner som vises i reaksjonsvelgeren"
reactionSettingDescription2: "Dra for å endre rekkefølgen, klikk for å slette, trykk \"+\" for å legge til."
rememberNoteVisibility: "Husk innstillingene for synlighet av Notes"
attachCancel: "Fjern vedlegg"
enterFileName: "Skriv inn filnavn"
mute: "Skjul"
unmute: "Vis"
renoteMute: "Skjul Renotes"
renoteUnmute: "Vis Renotes"
block: "Blokker"
unblock: "Opphev blokkering"
suspend: "Suspender"
blockConfirm: "Blokker?"
blockConfirm: "Er du sikker på at du vil blokke denne kontoen?"
unblockConfirm: "Er du sikker på at du vil oppheve blokkeringen av denne kontoen?"
suspendConfirm: "Er du sikker på at du vil suspendere denne kontoen?"
selectList: "Velg liste"
selectChannel: "Velg kanal"
selectList: "Velg en liste"
selectChannel: "Velg en kanal"
selectAntenna: "Velg en antenne"
selectWidget: "Velg en widget"
editWidgets: "Rediger widgeter"
@@ -113,6 +131,7 @@ flagAsBot: "Merk denne kontoen som en bot"
flagAsBotDescription: "Aktiver dette alternativet hvis denne kontoen styres av et program. Hvis det er aktivert, vil det fungere som et flagg for andre utviklere for å forhindre endeløse interaksjonskjeder med andre roboter og justere Misskeys interne systemer til å behandle denne kontoen som en bot."
flagAsCat: "Merk denne kontoen som en katt"
flagAsCatDescription: "Aktiver dette alternativet for å merke denne kontoen som en katt."
flagShowTimelineReplies: "Vis svar i tidslinje"
addAccount: "Legg til konto"
reloadAccountsList: "Last inn kontoliste på nytt"
loginFailed: "Kunne ikke logge inn"
@@ -120,23 +139,30 @@ general: "Generelt"
searchWith: "Søk: {q}"
youHaveNoLists: "Du har ingen lister"
followConfirm: "Er du sikker på at du vil følge {name}?"
selectUser: "Velg bruker"
host: "Vert"
selectUser: "Velg en bruker"
recipient: "Mottaker"
annotation: "Kommentarer"
federation: "Føderasjon"
instances: "Server"
instances: "Servere"
registeredAt: "Registrerte seg"
latestRequestReceivedAt: "Siste forespørsel mottatt"
latestStatus: "Siste status"
charts: "Diagrammer"
perHour: "Per time"
perDay: "Per dag"
stopActivityDelivery: "Slutt å sende aktiviteter"
blockThisInstance: "Blokker denne serveren"
operations: "Operasjoner"
software: "Programvare"
version: "Versjon"
metadata: "Metadata"
withNFiles: "{n} fil(er)"
network: "Nettverk"
instanceInfo: "Serverinformasjon"
statistics: "Statistikk"
clearQueue: "Tøm kø"
clearQueueConfirmTitle: "Vil du tømme kø?"
clearQueueConfirmTitle: "Er du sikker på at du vil tømme køen?"
blockedInstances: "Blokkerte severe"
blockedInstancesDescription: "Skriv opp vertsnavnene til serverne du vil blokkere, atskilt med linjeskift. Serverne i listen vil ikke lenger kunne kommunisere med denne serveren."
muteAndBlock: "Skjul og blokker"
@@ -144,9 +170,13 @@ mutedUsers: "Skjulte brukere"
blockedUsers: "Blokkerte brukere"
noUsers: "Det er ingen brukere"
editProfile: "Rediger profil"
noteDeleteConfirm: "Er du sikker på at du vil slette denne Noten?"
pinLimitExceeded: "Du kan ikke feste flere."
intro: "Installasjonen av Misskey er ferdig! Vennligst opprett en administratorkonto."
done: "Ferdig"
noCustomEmojis: "Ingen emoji"
default: "Standard"
defaultValueIs: "Standard: {value}"
noCustomEmojis: "Det er ingen emoji"
noJobs: "Det er ingen jobber"
blocked: "Blokkert"
suspended: "Suspendert"
@@ -154,54 +184,72 @@ all: "Alle"
notResponding: "Svarer ikke"
changePassword: "Endre passord"
security: "Sikkerhet"
retypedNotMatch: "Inngangene stemmer ikke overens."
currentPassword: "Nåværende passord"
newPassword: "Nytt passord"
newPasswordRetype: "Nytt passord (gjenta)"
attachFile: "Legg ved filer"
more: "Mer!"
noSuchUser: "Bruker ikke funnet"
announcements: "Kunngjøringer"
remove: "Slett"
removed: "Slettet"
removed: "Vellykket slettet"
removeAreYouSure: "Er du sikker på at du vil fjerne \"{x}\"?"
deleteAreYouSure: "Er du sikker på at du vil slette \"{x}\"?"
saved: "Lagret"
upload: "Laste opp"
keepOriginalUploading: "Behold originalbildet"
fromUrl: "Fra URL"
uploadFromUrl: "Last opp fra en URL"
uploadFromUrlDescription: "URL til filen du vil laste opp"
explore: "Utforsk"
messageRead: "Lest"
agree: "Jeg godtar"
nUsersRead: "lest av {n}"
agreeTo: "Jeg godtar {0}"
agree: "Godta"
agreeBelow: "Jeg godtar følgende"
basicNotesBeforeCreateAccount: "Viktige merknader"
termsOfService: "Vilkår for bruk"
home: "Hjem"
activity: "Aktivitet"
images: "Bilder"
image: "Bilder"
image: "Bilde"
birthday: "Bursdag"
yearsOld: "{age} år gammel"
theme: "Temaer"
light: "Lys"
dark: "Mørk"
lightThemes: "Lyse temaer"
darkThemes: "Mørke temaer"
syncDeviceDarkMode: "Synkroniser mørkmodus med enhetens innstillinger"
fileName: "Filnavn"
selectFile: "Velg fil"
selectFiles: "Velg fil"
selectFolder: "Velg mappe"
selectFolders: "Velg mappe"
selectFile: "Velg en fil"
selectFiles: "Velg filer"
selectFolder: "Velg en mappe"
selectFolders: "Velg mapper"
renameFile: "Endre filnavn"
folderName: "Mappenavn"
createFolder: "Opprett mappe"
createFolder: "Opprett en mappe"
renameFolder: "Endre mappenavn"
deleteFolder: "Slett mappe"
addFile: "Legg til fil"
deleteFolder: "Slett denne mappen"
addFile: "Legg til en fil"
emptyFolder: "Denne mappen er tom"
unableToDelete: "Kan ikke slette"
inputNewFileName: "Skriv inn et nytt filnavn"
inputNewDescription: "Skriv inn ny bildetekst"
inputNewFolderName: "Skriv inn et nytt mappenavn"
circularReferenceFolder: "Målmappen er en undermappe til mappen du ønsker å flytte."
hasChildFilesOrFolders: "Siden denne mappen ikke er tom, kan den ikke slettes."
copyUrl: "Kopier URL"
rename: "Endre navn"
avatar: "Avatar"
banner: "Banner"
doNothing: "Gjør ingenting"
doNothing: "Ignorer"
accept: "Tillatt"
reject: "Avslå"
instanceName: "Servernavn"
instanceDescription: "Serverbeskrivelse"
thisYear: "I år"
thisYear: "År"
thisMonth: "Måned"
today: "I dag"
dayX: "{day}"
@@ -216,23 +264,35 @@ registration: "Registrer"
enableRegistration: "Aktiver registrering av nye brukere"
invite: "Inviter"
basicInfo: "Grunnleggende informasjon"
pinnedUsers: "Festete brukrere"
pinnedUsers: "Festede brukrere"
pinnedUsersDescription: "Liste over brukernavn atskilt med linjeskift som skal festes i \"Utforsk\" fanen."
pinnedPages: "Festete sider"
pinnedPages: "Festede sider"
pinnedNotes: "Festet Note"
hcaptcha: "hCaptcha"
enableHcaptcha: "Aktiver hCaptcha"
recaptcha: "reCAPTCHA"
enableRecaptcha: "Aktiver reCAPTCHA"
turnstile: "Turnstile"
enableTurnstile: "Aktiver Turnstile"
antennas: "Antenner"
name: "Navn"
antennaSource: "Antennekilde"
notifyAntenna: "Varsle om nye Notes"
withFileAntenna: "Bare Notes med filer"
notesAndReplies: "Notes og svar"
popularUsers: "Populære brukere"
exploreUsersCount: "Det finnes {count} brukere"
exploreFediverse: "Utforsk Fediverse"
userList: "Lister"
about: "Infomasjon"
about: "Informasjon"
aboutMisskey: "Om Misskey"
newPasswordIs: "Det nye passordet er \"{password}\"."
share: "Del"
notFound: "Ikke funnet"
markAsReadAllNotifications: "Merk alle varsler som lest"
markAsReadAllUnreadNotes: "Merk alle Notes som lest"
help: "Hjelp"
inputMessageHere: "Skriv inn melding her"
close: "Lukk"
invites: "Inviter"
members: "Medlemmer"
@@ -240,30 +300,46 @@ title: "Tittel"
text: "Tekst"
next: "Neste"
retype: "Gjenta"
quoteAttached: "Sitat"
noMessagesYet: "Ingen meldinger ennå"
newMessageExists: "Det er nye meldinger"
onlyOneFileCanBeAttached: "Du kan bare legge ved én fil i en melding"
invitations: "Inviter"
available: "Tilgjengelig"
unavailable: "Utilgjengelig"
tooShort: "For kort"
tooLong: "For langt"
weakPassword: "Svakt passord"
normalPassword: "Gjennomsnittlig passord"
strongPassword: "Sterkt passord"
signinWith: "Logg inn med {x}"
signinFailed: "Kunne ikke logge inn. Det oppgitte brukernavnet eller passordet er feil."
or: "eller"
language: "Språk"
aboutX: "Om {x}"
category: "Kategorier"
category: "Kategori"
createAccount: "Opprett konto"
openImageInNewTab: "Åpne bilder i ny fane"
clientSettings: "Klientinnstillinger"
accountSettings: "Kontoinnstillinger"
objectStorageRegion: "Region"
objectStorageUseSSL: "Bruk SSL"
objectStorageUseProxy: "Bruk Proxy"
deleteAll: "Slett alt"
newNoteRecived: "Det er nye Notes"
listen: "Lytt"
none: "Ingen"
volume: "Volum"
chooseEmoji: "Velg emoji"
recentUsed: "Sist brukte"
install: "Installer"
uninstall: "Avinstaller"
nothing: "Ingenting"
deleteAllFiles: "Slett alle filer"
deleteAllFilesConfirm: "Vil du slette alle filer?"
deleteAllFilesConfirm: "Er du sikker på at du vil slette alle filer?"
userSuspended: "Denne brukeren har blitt suspendert."
accountDeleted: "Kontoen blir slettet"
accountDeletedDescription: "Denne kontoen blir slettet"
accountDeletedDescription: "Denne kontoen har blitt slettet."
menu: "Meny"
poll: "Avstemning"
description: "Beskrivelse"
@@ -274,6 +350,7 @@ small: "Liten"
notificationType: "Varseltype"
edit: "Rediger"
email: "E-post"
smtpHost: "Vert"
smtpUser: "Brukernavn"
smtpPass: "Passord"
userSaysSomething: "{name} sa noe"
@@ -289,16 +366,25 @@ reportAbuse: "Rappoter"
send: "Send"
openInNewTab: "Åpne i ny fane"
waitingFor: "Venter på {x}"
random: "Tilfeldig"
system: "System"
desktop: "Skrivebord"
i18nInfo: "Misskey oversettes til flere språk av frivillige. Du kan hjelpe til på {link}."
followingCount: "Følger"
followersCount: "Følgere"
yes: "Ja"
no: "Nei"
contact: "Kontakt"
developer: "Utvikler"
makeExplorable: "Gjør konto synlig i \"Utforsk\""
makeExplorableDescription: "Hvis du slår av dette, vises ikke kontoen din i \"Utforsk\" delen."
left: "Venstre"
nNotes: "{n} Notes"
saveAs: "Lagre som"
value: "Verdi"
deleteConfirm: "Vil du slette?"
invalidValue: "Verdien er ugyldig."
closeAccount: "Avslutt konto"
emailNotification: "E-postvarsler"
inChannelSearch: "Søk i kanal"
clear: "Tøm"
@@ -312,17 +398,23 @@ accounts: "Kontoer"
switch: "Bytt"
gallery: "Galleri"
ads: "Annonser"
memo: "Notat"
high: "Høy"
low: "Lav"
sent: "Send"
sent: "Sendt"
received: "Mottatt"
learnMore: "Les mer"
misskeyUpdated: "Misskey har blitt oppdatert!"
translate: "Oversett"
translatedFrom: "Oversatt fra {x}"
unread: "Ulest"
manageAccounts: "Administrer konto"
classic: "Klassisk"
muteThread: "Skjul denne tråden"
unmuteThread: "Vis denne tråden"
continueThread: "Vis fortsettelse av tråden"
hide: "Skjul"
smartphone: "Smarttelefon"
tablet: "Nettbrett"
auto: "Automatisk"
size: "Størrelse"
@@ -338,10 +430,10 @@ check: "Sjekk"
deleteAccount: "Slett konto"
document: "Dokumenter"
logoutConfirm: "Vil du logge ut?"
pleaseSelect: "Vennligst velg"
pleaseSelect: "Velg et alternativ"
type: "Type"
beta: "Beta"
account: "Kontoer"
account: "Konto"
move: "Flytt"
pushNotification: "Push-varsler"
tools: "Verktøy"
@@ -357,6 +449,7 @@ role: "Rolle"
color: "Farge"
youCannotCreateAnymore: "Du kan ikke opprette flere."
cannotPerformTemporary: "Midlertidig utilgjengelig"
achievements: "Prestasjoner"
thisPostMayBeAnnoyingCancel: "Avbryt"
exploreOtherServers: "Utforsk andre severe"
letsLookAtTimeline: "La oss se på tidslinje"
@@ -372,6 +465,26 @@ _initialAccountSetting:
theseSettingsCanEditLater: "Du kan endre disse innstillingene senere."
_achievements:
_types:
_notes10:
title: "Noen Notes"
_notes100:
title: "Mange Notes"
_notes500:
title: "Dekket i Notes"
_notes1000:
title: "Et fjell av Notes"
_notes5000:
title: "Overfylte Notes"
_notes10000:
title: "Super Notes"
_notes20000:
title: "Trenger... mer... Notes..."
_notes30000:
title: "Notes Notes Notes!"
_notes40000:
title: "Note fabrikk"
_notes50000:
title: "Planet av Notes"
_notes100000:
flavor: "Du har jammen mye å si."
_noteFavorited1:
@@ -400,7 +513,7 @@ _achievements:
_justPlainLucky:
title: "Rett og slett heldig"
_setNameToSyuilo:
description: "Du har satt navnet ditt til \"syuilo\""
description: "Du satte navnet ditt til \"syuilo\""
_passedSinceAccountCreated1:
title: "Ett års jubileum"
description: "Det har gått ett år siden kontoen din ble opprettet"
@@ -468,15 +581,17 @@ _theme:
key: "Nøkkel"
keys:
link: "Lenke"
renote: "Renote"
_sfx:
note: "Notes"
notification: "Varsler"
_ago:
future: "Fremitid"
justNow: "Akkurat nå"
secondsAgo: "{n} sekunder siden"
minutesAgo: "{n} minutter siden"
hoursAgo: "{n} timer siden"
daysAgo: "{n} dager siden"
secondsAgo: "{n}s siden"
minutesAgo: "{n}m siden"
hoursAgo: "{n}t siden"
daysAgo: "{n}d siden"
weeksAgo: "{n} uker siden"
monthsAgo: "{n} måneder siden"
yearsAgo: "{n} år siden"
@@ -488,6 +603,7 @@ _time:
day: "Dager"
_timelineTutorial:
title: "Hvordan bruke Misskey"
step2_2: "Hva med å skrive en selvpresentasjon, eller bare \"Hei {name}!\" hvis du ikke har lyst?"
_2fa:
renewTOTPCancel: "Avbryt"
_weekday:
@@ -500,6 +616,7 @@ _weekday:
saturday: "Lørdag"
_widgets:
profile: "Profil"
instanceInfo: "Serverinformasjon"
notifications: "Varsler"
timeline: "Tidslinje"
calendar: "Kalender"
@@ -535,6 +652,7 @@ _postForm:
_profile:
name: "Navn"
username: "Brukernavn"
description: "Biografi"
metadataContent: "Innhold"
_exportOrImport:
followingList: "Følg"
@@ -576,13 +694,17 @@ _pages:
button: "Knapp"
_notification:
youWereFollowed: "fulgte deg"
unreadAntennaNote: "Antenne {name}"
achievementEarned: "Prestasjon låst opp"
_types:
follow: "Følg"
follow: "Nye følgere"
reply: "Svar"
quote: "Sitat"
reaction: "Reaksjon"
renote: "Renotes"
quote: "Sitater"
reaction: "Reaksjoner"
_actions:
reply: "Svar"
renote: "Renote"
_deck:
swapLeft: "Flytt til venstre"
swapRight: "Flytt til høyre"
@@ -594,6 +716,7 @@ _deck:
_columns:
notifications: "Varsler"
tl: "Tidslinje"
antenna: "Antenner"
list: "Lister"
channel: "Kanaler"
direct: "Direkte"

View File

@@ -2,7 +2,7 @@
_lang_: "Русский"
headlineMisskey: "Сеть, сплетённая из заметок"
introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀"
poweredByMisskeyDescription: "{name} один из инстансов (также называемый экземпляром Misskey), использующий платформу с открытым исходным кодом <b>Misskey</b>."
poweredByMisskeyDescription: "{name} сервис на платформе с открытым исходным кодом <b>Misskey</b>, называемый инстансом Misskey."
monthAndDay: "{day}.{month}"
search: "Поиск"
notifications: "Уведомления"
@@ -649,8 +649,8 @@ abuseReported: "Жалоба отправлена. Большое спасибо
reporter: "Сообщивший"
reporteeOrigin: "О ком сообщено"
reporterOrigin: "Кто сообщил"
forwardReport: "Перенаправление отчета на инстант."
forwardReportIsAnonymous: "Удаленный инстант не сможет увидеть вашу информацию и будет отображаться как анонимная системная учетная запись."
forwardReport: "Отправить жалобу на инстанс автора."
forwardReportIsAnonymous: "Жалоба на удалённый инстанс будет отправлена анонимно. Вместо ваших данных у получателя будет отображена системная учётная запись."
send: "Отправить"
abuseMarkAsResolved: "Отметить жалобу как решённую"
openInNewTab: "Открыть в новой вкладке"
@@ -823,6 +823,7 @@ translatedFrom: "Перевод. Язык оригинала — {x}"
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
aiChanMode: "Режим Ай"
devMode: "Режим разработчика"
keepCw: "Сохраняйте Предупреждения о содержимом"
pubSub: "Учётные записи Pub/Sub"
lastCommunication: "Последнее сообщение"
@@ -914,8 +915,8 @@ cannotUploadBecauseInappropriate: "Файл не может быть загру
cannotUploadBecauseNoFreeSpace: "Файл не может быть загружен, так как не осталось места на диске"
cannotUploadBecauseExceedsFileSizeLimit: "Файл не может быть загружен, так как он превышает лимит размера файла."
beta: "Бета"
enableAutoSensitive: "Автоматическое определение NSFW"
enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена ​​автоматически в зависимости от инстанта."
enableAutoSensitive: "Автоматическое определение содержимого не для всех"
enableAutoSensitiveDescription: "Позволяет определять наличие содержимого не для всех при помощи искусственного интеллекта там, где это возможно. Даже если эту опцию отключить, она всё равно может быть включена на весь инстанс."
activeEmailValidationDescription: "Если включено, будет проводиться более строгая проверка адреса электронной почты, в том числе на то, что он действительный и не временный. Если же отключено, то проверяется только корректность написания адреса."
navbar: "Панель навигации"
shuffle: "Перемешать"
@@ -1006,6 +1007,7 @@ noteIdOrUrl: "ID или ссылка на заметку"
video: "Видео"
videos: "Видео"
dataSaver: "Экономия трафика"
renotesList: "Репосты"
horizontal: "Сбоку"
youFollowing: "Подписки"
options: "Настройки ролей"
@@ -1180,6 +1182,9 @@ _achievements:
_client30min:
title: "Перерыв на обед"
description: "Прошло 30 минут с момента запуска клиента"
_client60min:
title: "Не наглядеться на Misskey"
description: "Misskey был открыт 60 минут подряд"
_noteDeletedWithin1min:
title: "Ой, нет!"
description: "Заметка удалена через минуту после публикации"
@@ -1282,6 +1287,7 @@ _role:
canInvite: "Может создавать пригласительные коды"
canManageCustomEmojis: "Управлять пользовательскими эмодзи"
driveCapacity: "Доступное пространство на «диске»"
alwaysMarkNsfw: "Всегда отмечать файлы как «не для всех»"
pinMax: "Доступное количество закреплённых заметок"
antennaMax: "Доступное количество антенн"
wordMuteMax: "Доступное количество знаков в списке скрытия слов"
@@ -1309,7 +1315,7 @@ _sensitiveMediaDetection:
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
sensitivity: "Чувствительность обнаружения"
sensitivityDescription: "Более низкая чувствительность уменьшает количество ложных срабатываний (false positives). Повышение чувствительности уменьшает утечку при обнаружении (ложноотрицательные результаты)."
setSensitiveFlagAutomatically: "Установить флаг NSFW"
setSensitiveFlagAutomatically: "Обозначить как не для всех"
setSensitiveFlagAutomaticallyDescription: "Даже если этот параметр отключен, результат оценки сохраняется внутри системы."
analyzeVideos: "Анализировать видео?"
analyzeVideosDescription: "Анализируйте видео в дополнение к неподвижным изображениям. Нагрузка на сервер немного увеличивается."
@@ -1528,6 +1534,16 @@ _time:
minute: "мин"
hour: "ч"
day: "сут"
_timelineTutorial:
title: "Как пользоваться Misskey"
step1_1: "Это лицо Misskey, так называемая лента. Ваш инстанс, {name}, покажет тут все опубликованные на нём заметки в хронологическом порядке."
step1_2: "Здесь есть несколько лент. К примеру «персональная» лента отображает заметки тех, на кого вы подписаны. А «местная» — заметки тех, кого приютил {name}."
step2_1: "Что ж, теперь самое время опубликовать заметку. Если нажать вверху страницы на изображение карандаша, появится форма для текста."
step2_2: "Почему бы не написать немного о себе? Ну, или хотя бы «Привет, {name}»?"
step3_1: "Справились с первой заметкой?"
step3_2: "Отлично, теперь она должна появиться в вашей ленте."
step4_1: "А ещё здесь можно делиться своими реакциями на заметки."
step4_2: "Отмечайте реакции, нажимая на символ «+» под заметкой и выбирая значок по душе."
_2fa:
alreadyRegistered: "Двухфакторная аутентификация уже настроена."
registerTOTP: "Начните настраивать приложение-аутентификатор"
@@ -1868,6 +1884,9 @@ _deck:
_dialog:
charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}"
charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}"
_disabledTimeline:
title: "Лента отключена"
description: "Ваша текущая роль не позволяет пользоваться этой лентой."
_webhookSettings:
name: "Название"
active: "Вкл."

View File

@@ -52,6 +52,8 @@ addToList: "添加至列表"
sendMessage: "发送"
copyRSS: "复制RSS"
copyUsername: "复制用户名"
copyUserId: "复制用户ID"
copyNoteId: "复制帖子ID"
searchUser: "搜索用户"
reply: "回复"
loadMore: "查看更多"
@@ -790,6 +792,7 @@ noMaintainerInformationWarning: "管理人员信息未设置。"
noBotProtectionWarning: "Bot保护未设置。"
configure: "设置"
postToGallery: "发送到图库"
postToHashtag: "投稿到这个标签"
gallery: "图库"
recentPosts: "最新发布"
popularPosts: "热门投稿"
@@ -823,6 +826,7 @@ translatedFrom: "从 {x} 翻译"
accountDeletionInProgress: "正在删除账户"
usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。"
aiChanMode: "小蓝模式"
devMode: "开发者模式"
keepCw: "回复时维持隐藏内容"
pubSub: "Pub/Sub账户"
lastCommunication: "最近通信"
@@ -832,6 +836,8 @@ breakFollow: "移除关注者"
breakFollowConfirm: "你想取消关注吗?"
itsOn: "已开启"
itsOff: "已关闭"
on: "开启"
off: "关闭"
emailRequiredForSignup: "注册账户需要电子邮件地址"
unread: "未读"
filter: "筛选"
@@ -986,6 +992,8 @@ cannotBeChangedLater: "之后不能再更改。"
reactionAcceptance: "接受表情回应"
likeOnly: "仅点赞"
likeOnlyForRemote: "远程仅点赞"
nonSensitiveOnly: "仅限非敏感内容"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点赞)"
rolesAssignedToMe: "指派给自己的角色"
resetPasswordConfirm: "确定重置密码?"
sensitiveWords: "敏感词"
@@ -1043,6 +1051,16 @@ preventAiLearning: "拒绝接受生成式AI的学习"
preventAiLearningDescription: "要求文章生成AI或图像生成AI不能够以发布的帖子和图像等内容作为学习对象。这是通过在HTML响应中包含noai标志来实现的这不能完全阻止AI学习你的发布内容并不是所有AI都会遵守这类请求。"
options: "选项"
specifyUser: "用户指定"
failedToPreviewUrl: "无法预览"
update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "可以使用表情作为回应的角色"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "在没有指定角色的情况下,任何人都可以使用表情作为回应。"
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "角色必须是公开的。"
cancelReactionConfirm: "要取消回应吗?"
changeReactionConfirm: "要更改回应吗?"
later: "一会再说"
goToMisskey: "去往Misskey"
installed: "已安装"
_initialAccountSetting:
accountCreated: "账户创建完成了!"
letsStartAccountSetup: "来进行帐户的初始设置吧。"
@@ -1057,6 +1075,7 @@ _initialAccountSetting:
haveFun: "希望{name}在这里玩得开心!"
ifYouNeedLearnMore: "关于{name}(Misskey)的使用方法,详见{link}。"
skipAreYouSure: "要跳过初始设置吗?"
laterAreYouSure: "要稍后再进行初始设定吗?"
_serverRules:
description: "在新用户注册前显示服务器的简单规则。推荐显示服务条款的主要内容。"
_accountMigration:

View File

@@ -1055,6 +1055,13 @@ failedToPreviewUrl: "無法預覽"
update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "可以當成反應使用的角色"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "如果是未指定角色的情況,則任何人都可以被當成反應來使用。"
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "角色必須是公開的角色。"
cancelReactionConfirm: "要取消做出的反應嗎?"
changeReactionConfirm: "要變更做出的反應嗎?"
later: "稍後再說"
goToMisskey: "往Misskey"
additionalEmojiDictionary: "表情符號的附加辭典"
installed: "已安裝"
_initialAccountSetting:
accountCreated: "帳戶已建立完成!"
letsStartAccountSetup: "來進行帳戶的初始設定吧。"
@@ -1069,6 +1076,7 @@ _initialAccountSetting:
haveFun: "盡情享受{name}吧!"
ifYouNeedLearnMore: "關於如何使用{name}(Misskey)的詳細資訊,請見{link}。"
skipAreYouSure: "要略過初始設定嗎?"
laterAreYouSure: "稍後再重新進行初始設定嗎?"
_serverRules:
description: "設定伺服器的簡要規則,在新的註冊之前顯示。建議的內容是使用條款的摘要。"
_accountMigration:

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "13.13.0-beta.5",
"version": "13.13.0",
"codename": "nasubi",
"repository": {
"type": "git",
@@ -51,16 +51,16 @@
"gulp-replace": "1.1.4",
"gulp-terser": "2.1.0",
"js-yaml": "4.1.0",
"typescript": "5.0.4"
"typescript": "5.1.3"
},
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5",
"@typescript-eslint/eslint-plugin": "5.59.8",
"@typescript-eslint/parser": "5.59.8",
"cross-env": "7.0.3",
"cypress": "12.13.0",
"eslint": "8.40.0",
"eslint": "8.41.0",
"start-server-and-test": "2.0.0"
},
"optionalDependencies": {

View File

@@ -60,27 +60,27 @@
"@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.1",
"@fastify/cors": "8.3.0",
"@fastify/http-proxy": "9.1.0",
"@fastify/multipart": "7.6.0",
"@fastify/static": "6.10.1",
"@fastify/static": "6.10.2",
"@fastify/view": "7.4.1",
"@nestjs/common": "9.4.2",
"@nestjs/core": "9.4.2",
"@nestjs/testing": "9.4.2",
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2",
"@sinonjs/fake-timers": "10.2.0",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.59",
"@swc/core": "1.3.61",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
"autwh": "0.1.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"bullmq": "3.14.1",
"bullmq": "3.15.0",
"cacheable-lookup": "6.1.0",
"cbor": "8.1.0",
"cbor": "9.0.0",
"chalk": "5.2.0",
"chalk-template": "0.4.0",
"chokidar": "3.5.3",
@@ -96,24 +96,24 @@
"fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0",
"got": "12.6.0",
"happy-dom": "9.19.2",
"happy-dom": "9.20.3",
"hpagent": "1.2.0",
"ioredis": "5.3.2",
"ip-cidr": "3.1.0",
"is-svg": "4.3.2",
"js-yaml": "4.1.0",
"jsdom": "21.1.1",
"jsdom": "22.1.0",
"json5": "2.2.3",
"jsonld": "8.1.1",
"jsonld": "8.2.0",
"jsrsasign": "10.8.6",
"meilisearch": "0.32.4",
"meilisearch": "0.32.5",
"mfm-js": "0.23.3",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"ms": "3.0.0-canary.1",
"nested-property": "4.0.0",
"node-fetch": "3.3.1",
"nodemailer": "6.9.2",
"nodemailer": "6.9.3",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"os-utils": "0.0.14",
@@ -129,7 +129,7 @@
"qrcode": "1.5.3",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.18.0",
"re2": "1.19.0",
"redis-lock": "0.1.4",
"reflect-metadata": "0.1.13",
"rename": "1.0.4",
@@ -146,16 +146,16 @@
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly",
"systeminformation": "5.17.12",
"systeminformation": "5.17.16",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.6",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.16",
"typescript": "5.0.4",
"typescript": "5.1.3",
"ulid": "2.3.0",
"unzipper": "0.10.11",
"unzipper": "0.10.14",
"uuid": "9.0.0",
"vary": "1.1.2",
"web-push": "3.6.1",
@@ -173,13 +173,13 @@
"@types/content-disposition": "0.5.5",
"@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21",
"@types/jest": "29.5.1",
"@types/jest": "29.5.2",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.1",
"@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1",
"@types/node": "20.2.3",
"@types/node": "20.2.5",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.8",
"@types/oauth": "0.9.1",
@@ -203,11 +203,11 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5",
"@typescript-eslint/eslint-plugin": "5.59.8",
"@typescript-eslint/parser": "5.59.8",
"aws-sdk-client-mock": "2.1.1",
"cross-env": "7.0.3",
"eslint": "8.40.0",
"eslint": "8.41.0",
"eslint-plugin-import": "2.27.5",
"execa": "6.1.0",
"jest": "29.5.0",

View File

@@ -19,6 +19,8 @@ import type * as http from 'node:http';
@Injectable()
export class StreamingApiServerService {
#wss: WebSocket.WebSocketServer;
#connections = new Map<WebSocket.WebSocket, number>();
#cleanConnectionsIntervalId: NodeJS.Timeout | null = null;
constructor(
@Inject(DI.config)
@@ -109,7 +111,9 @@ export class StreamingApiServerService {
await stream.listen(ev, connection);
const intervalId = user ? setInterval(() => {
this.#connections.set(connection, Date.now());
const userUpdateIntervalId = user ? setInterval(() => {
this.usersRepository.update(user.id, {
lastActiveDate: new Date(),
});
@@ -124,19 +128,34 @@ export class StreamingApiServerService {
ev.removeAllListeners();
stream.dispose();
this.redisForSub.off('message', onRedisMessage);
if (intervalId) clearInterval(intervalId);
if (userUpdateIntervalId) clearInterval(userUpdateIntervalId);
});
connection.on('message', async (data) => {
this.#connections.set(connection, Date.now());
if (data.toString() === 'ping') {
connection.send('pong');
}
});
});
this.#cleanConnectionsIntervalId = setInterval(() => {
const now = Date.now();
for (const [connection, lastActive] of this.#connections.entries()) {
if (now - lastActive > 1000 * 60 * 5) {
connection.terminate();
this.#connections.delete(connection);
}
}
}, 1000 * 60 * 5);
}
@bindThis
public detach(): Promise<void> {
if (this.#cleanConnectionsIntervalId) {
clearInterval(this.#cleanConnectionsIntervalId);
this.#cleanConnectionsIntervalId = null;
}
return new Promise((resolve) => {
this.#wss.close(() => resolve());
});

View File

@@ -113,6 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
this.antennasRepository.update(antenna.id, {
isActive: true,
lastUsedAt: new Date(),
});

View File

@@ -1,6 +1,6 @@
import { promisify } from 'node:util';
import bcrypt from 'bcryptjs';
import * as cbor from 'cbor';
import cbor from 'cbor';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';

View File

@@ -91,18 +91,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const notificationsRes = await this.redisClient.xrevrange(
`notificationTimeline:${me.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-',
'COUNT', limit);
if (notificationsRes.length === 0) {
return [];
}
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId) as Notification[];
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as Notification[];
if (includeTypes && includeTypes.length > 0) {
notifications = notifications.filter(notification => includeTypes.includes(notification.type));

View File

@@ -146,7 +146,7 @@ export const paramDef = {
alwaysMarkNsfw: { type: 'boolean' },
autoSensitive: { type: 'boolean' },
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
pinnedPageId: { type: 'string', format: 'misskey:id' },
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
mutedWords: { type: 'array' },
mutedInstances: { type: 'array', items: {
type: 'string',

View File

@@ -116,9 +116,9 @@
}
}
}
const colorSchema = localStorage.getItem('colorSchema');
if (colorSchema) {
document.documentElement.style.setProperty('color-schema', colorSchema);
const colorScheme = localStorage.getItem('colorScheme');
if (colorScheme) {
document.documentElement.style.setProperty('color-scheme', colorScheme);
}
//#endregion

View File

@@ -35,7 +35,7 @@ html
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
//- https://github.com/misskey-dev/misskey/issues/9842
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.17.0')
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.21.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists

View File

@@ -2,7 +2,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as crypto from 'node:crypto';
import * as cbor from 'cbor';
import cbor from 'cbor';
import * as OTPAuth from 'otpauth';
import { loadConfig } from '../../src/config.js';
import { signup, api, post, react, startServer, waitFire } from '../utils.js';

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/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.12.0/tabler-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.21.0/tabler-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
<style>
html {

View File

@@ -0,0 +1,597 @@
import { parse } from 'acorn';
import { generate } from 'astring';
import { describe, expect, it } from 'vitest';
import { normalizeClass, unwindCssModuleClassName } from './rollup-plugin-unwind-css-module-class-name';
import type * as estree from 'estree';
function parseExpression(code: string): estree.Expression {
const program = parse(code, { ecmaVersion: 'latest', sourceType: 'module' }) as unknown as estree.Program;
const statement = program.body[0] as estree.ExpressionStatement;
return statement.expression;
}
describe(normalizeClass.name, () => {
it('should normalize string', () => {
expect(normalizeClass(parseExpression('"a b c"'))).toBe('a b c');
});
it('should trim redundant spaces', () => {
expect(normalizeClass(parseExpression('" a b c "'))).toBe('a b c');
});
it('should ignore undefined', () => {
expect(normalizeClass(parseExpression('undefined'))).toBe('');
});
it('should ignore non string literals', () => {
expect(normalizeClass(parseExpression('0'))).toBe('');
expect(normalizeClass(parseExpression('true'))).toBe('');
expect(normalizeClass(parseExpression('null'))).toBe('');
expect(normalizeClass(parseExpression('/I.D/'))).toBe('');
});
it('should not normalize identifiers', () => {
expect(normalizeClass(parseExpression('EScape'))).toBeNull();
});
it('should normalize recursively array', () => {
expect(normalizeClass(parseExpression('["from", ...["Utopia"]]'))).toBe('from Utopia');
expect(normalizeClass(parseExpression('["from", ...[Utopia]]'))).toBeNull();
});
it('should normalize recursively template literal', () => {
expect(normalizeClass(parseExpression('`name ${"shiho"} code ${33}`'))).toBe('name shiho code');
expect(normalizeClass(parseExpression('`name ${shiho.name} code ${33}`'))).toBeNull();
});
it('should normalize recursively binary expression', () => {
expect(normalizeClass(parseExpression('"mirage" + "mirror"'))).toBe('miragemirror');
expect(normalizeClass(parseExpression('"mirage" + mirror'))).toBeNull();
});
it('should normalize recursively object expression', () => {
expect(normalizeClass(parseExpression('({ a: true, b: "c" })'))).toBe('a b');
expect(normalizeClass(parseExpression('({ a: false, b: "c" })'))).toBe('b');
expect(normalizeClass(parseExpression('({ a: true, b: c })'))).toBeNull();
expect(normalizeClass(parseExpression('({ a: true, b: "c", ...({ d: true }) })'))).toBe('a b d');
expect(normalizeClass(parseExpression('({ a: true, [b]: "c" })'))).toBeNull();
expect(normalizeClass(parseExpression('({ a: true, b: false, c: !false, d: !!0 })'))).toBe('a c');
});
});
it('Composition API (standard)', () => {
const ast = parse(`
import { c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js';
import { M as MkContainer } from './MkContainer-!~{03M}~.js';
import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js';
import './photoswipe-!~{003}~.js';
const _hoisted_1 = /* @__PURE__ */ createBaseVNode("i", { class: "ti ti-photo" }, null, -1);
const _sfc_main = /* @__PURE__ */ defineComponent({
__name: "index.photos",
props: {
user: {}
},
setup(__props) {
const props = __props;
let fetching = ref(true);
let images = ref([]);
function thumbnail(image) {
return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
}
onMounted(() => {
const image = [
"image/jpeg",
"image/webp",
"image/avif",
"image/png",
"image/gif",
"image/apng",
"image/vnd.mozilla.apng"
];
api("users/notes", {
userId: props.user.id,
fileType: image,
excludeNsfw: defaultStore.state.nsfw !== "ignore",
limit: 10
}).then((notes) => {
for (const note of notes) {
for (const file of note.files) {
images.value.push({
note,
file
});
}
}
fetching.value = false;
});
});
return (_ctx, _cache) => {
const _component_MkLoading = resolveComponent("MkLoading");
const _component_MkA = resolveComponent("MkA");
return openBlock(), createBlock(MkContainer, {
"max-height": 300,
foldable: true
}, {
icon: withCtx(() => [
_hoisted_1
]),
header: withCtx(() => [
createTextVNode(toDisplayString(unref(i18n).ts.images), 1)
]),
default: withCtx(() => [
createBaseVNode("div", {
class: normalizeClass(_ctx.$style.root)
}, [
unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, { key: 0 })) : createCommentVNode("", true),
!unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
key: 1,
class: normalizeClass(_ctx.$style.stream)
}, [
(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), (image) => {
return openBlock(), createBlock(_component_MkA, {
key: image.note.id + image.file.id,
class: normalizeClass(_ctx.$style.img),
to: unref(notePage)(image.note)
}, {
default: withCtx(() => [
createVNode(ImgWithBlurhash, {
hash: image.file.blurhash,
src: thumbnail(image.file),
title: image.file.name
}, null, 8, ["hash", "src", "title"])
]),
_: 2
}, 1032, ["class", "to"]);
}), 128))
], 2)) : createCommentVNode("", true),
!unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", {
key: 2,
class: normalizeClass(_ctx.$style.empty)
}, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)
], 2)
]),
_: 1
});
};
}
});
const root = "xenMW";
const stream = "xaZzf";
const img = "xtA8t";
const empty = "xhYKj";
const style0 = {
root: root,
stream: stream,
img: img,
empty: empty
};
const cssModules = {
"$style": style0
};
const index_photos = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
export { index_photos as default };
`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
unwindCssModuleClassName(ast);
expect(generate(ast)).toBe(`
import {c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js';
import {M as MkContainer} from './MkContainer-!~{03M}~.js';
import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js';
import './photoswipe-!~{003}~.js';
const _hoisted_1 = createBaseVNode("i", {
class: "ti ti-photo"
}, null, -1);
const _sfc_main = defineComponent({
__name: "index.photos",
props: {
user: {}
},
setup(__props) {
const props = __props;
let fetching = ref(true);
let images = ref([]);
function thumbnail(image) {
return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
}
onMounted(() => {
const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"];
api("users/notes", {
userId: props.user.id,
fileType: image,
excludeNsfw: defaultStore.state.nsfw !== "ignore",
limit: 10
}).then(notes => {
for (const note of notes) {
for (const file of note.files) {
images.value.push({
note,
file
});
}
}
fetching.value = false;
});
});
return (_ctx, _cache) => {
const _component_MkLoading = resolveComponent("MkLoading");
const _component_MkA = resolveComponent("MkA");
return (openBlock(), createBlock(MkContainer, {
"max-height": 300,
foldable: true
}, {
icon: withCtx(() => [_hoisted_1]),
header: withCtx(() => [createTextVNode(toDisplayString(unref(i18n).ts.images), 1)]),
default: withCtx(() => [createBaseVNode("div", {
class: "xenMW"
}, [unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, {
key: 0
})) : createCommentVNode("", true), !unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
key: 1,
class: "xaZzf"
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), image => {
return (openBlock(), createBlock(_component_MkA, {
key: image.note.id + image.file.id,
class: "xtA8t",
to: unref(notePage)(image.note)
}, {
default: withCtx(() => [createVNode(ImgWithBlurhash, {
hash: image.file.blurhash,
src: thumbnail(image.file),
title: image.file.name
}, null, 8, ["hash", "src", "title"])]),
_: 2
}, 1032, ["class", "to"]));
}), 128))], 2)) : createCommentVNode("", true), !unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", {
key: 2,
class: "xhYKj"
}, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)], 2)]),
_: 1
}));
};
}
});
const root = "xenMW";
const stream = "xaZzf";
const img = "xtA8t";
const empty = "xhYKj";
const style0 = {
root: root,
stream: stream,
img: img,
empty: empty
};
const cssModules = {
"$style": style0
};
const index_photos = _sfc_main;
export {index_photos as default};
`.slice(1));
});
it('Composition API (with `useCssModule()`)', () => {
const ast = parse(`
import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js';
import { d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js';
function isDebuggerEnabled(id) {
try {
return localStorage.getItem(\`DEBUG_\${id}\`) !== null;
} catch {
return false;
}
}
function stackTraceInstances() {
let instance = getCurrentInstance();
const stack = [];
while (instance) {
stack.push(instance);
instance = instance.parent;
}
return stack;
}
const _sfc_main = defineComponent({
props: {
items: {
type: Array,
required: true
},
direction: {
type: String,
required: false,
default: "down"
},
reversed: {
type: Boolean,
required: false,
default: false
},
noGap: {
type: Boolean,
required: false,
default: false
},
ad: {
type: Boolean,
required: false,
default: false
}
},
setup(props, { slots, expose }) {
const $style = useCssModule();
function getDateText(time) {
const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1;
return i18n.t("monthAndDay", {
month: month.toString(),
day: date.toString()
});
}
if (props.items.length === 0)
return;
const renderChildrenImpl = () => props.items.map((item, i) => {
if (!slots || !slots.default)
return;
const el = slots.default({
item
})[0];
if (el.key == null && item.id)
el.key = item.id;
if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) {
const separator = h("div", {
class: $style["separator"],
key: item.id + ":separator"
}, h("p", {
class: $style["date"]
}, [
h("span", {
class: $style["date-1"]
}, [
h("i", {
class: \`ti ti-chevron-up \${$style["date-1-icon"]}\`
}),
getDateText(item.createdAt)
]),
h("span", {
class: $style["date-2"]
}, [
getDateText(props.items[i + 1].createdAt),
h("i", {
class: \`ti ti-chevron-down \${$style["date-2-icon"]}\`
})
])
]));
return [el, separator];
} else {
if (props.ad && item._shouldInsertAd_) {
return [h(MkAd, {
key: item.id + ":ad",
prefer: ["horizontal", "horizontal-big"]
}), el];
} else {
return el;
}
}
});
const renderChildren = () => {
const children = renderChildrenImpl();
if (isDebuggerEnabled(6864)) {
const nodes = children.flatMap((node) => node ?? []);
const keys = new Set(nodes.map((node) => node.key));
if (keys.size !== nodes.length) {
const id = crypto.randomUUID();
const instances = stackTraceInstances();
toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`));
console.warn({ id, debugId: 6864, stack: instances });
}
}
return children;
};
function onBeforeLeave(el) {
el.style.top = \`\${el.offsetTop}px\`;
el.style.left = \`\${el.offsetLeft}px\`;
}
function onLeaveCanceled(el) {
el.style.top = "";
el.style.left = "";
}
return () => h(
defaultStore.state.animation ? TransitionGroup : "div",
{
class: {
[$style["date-separated-list"]]: true,
[$style["date-separated-list-nogap"]]: props.noGap,
[$style["reversed"]]: props.reversed,
[$style["direction-down"]]: props.direction === "down",
[$style["direction-up"]]: props.direction === "up"
},
...defaultStore.state.animation ? {
name: "list",
tag: "div",
onBeforeLeave,
onLeaveCanceled
} : {}
},
{ default: renderChildren }
);
}
});
const reversed = "xxiZh";
const separator = "xxeDx";
const date = "xxawD";
const style0 = {
"date-separated-list": "xfKPa",
"date-separated-list-nogap": "xf9zr",
"direction-up": "x7AeO",
"direction-down": "xBIqc",
reversed: reversed,
separator: separator,
date: date,
"date-1": "xwtmh",
"date-1-icon": "xsNPa",
"date-2": "x1xvw",
"date-2-icon": "x9ZiG"
};
const cssModules = {
"$style": style0
};
const MkDateSeparatedList = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
export { MkDateSeparatedList as M };
`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
unwindCssModuleClassName(ast);
expect(generate(ast)).toBe(`
import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js';
import {d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js';
function isDebuggerEnabled(id) {
try {
return localStorage.getItem(\`DEBUG_\${id}\`) !== null;
} catch {
return false;
}
}
function stackTraceInstances() {
let instance = getCurrentInstance();
const stack = [];
while (instance) {
stack.push(instance);
instance = instance.parent;
}
return stack;
}
const _sfc_main = defineComponent({
props: {
items: {
type: Array,
required: true
},
direction: {
type: String,
required: false,
default: "down"
},
reversed: {
type: Boolean,
required: false,
default: false
},
noGap: {
type: Boolean,
required: false,
default: false
},
ad: {
type: Boolean,
required: false,
default: false
}
},
setup(props, {slots, expose}) {
const $style = useCssModule();
function getDateText(time) {
const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1;
return i18n.t("monthAndDay", {
month: month.toString(),
day: date.toString()
});
}
if (props.items.length === 0) return;
const renderChildrenImpl = () => props.items.map((item, i) => {
if (!slots || !slots.default) return;
const el = slots.default({
item
})[0];
if (el.key == null && item.id) el.key = item.id;
if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) {
const separator = h("div", {
class: $style["separator"],
key: item.id + ":separator"
}, h("p", {
class: $style["date"]
}, [h("span", {
class: $style["date-1"]
}, [h("i", {
class: \`ti ti-chevron-up \${$style["date-1-icon"]}\`
}), getDateText(item.createdAt)]), h("span", {
class: $style["date-2"]
}, [getDateText(props.items[i + 1].createdAt), h("i", {
class: \`ti ti-chevron-down \${$style["date-2-icon"]}\`
})])]));
return [el, separator];
} else {
if (props.ad && item._shouldInsertAd_) {
return [h(MkAd, {
key: item.id + ":ad",
prefer: ["horizontal", "horizontal-big"]
}), el];
} else {
return el;
}
}
});
const renderChildren = () => {
const children = renderChildrenImpl();
if (isDebuggerEnabled(6864)) {
const nodes = children.flatMap(node => node ?? []);
const keys = new Set(nodes.map(node => node.key));
if (keys.size !== nodes.length) {
const id = crypto.randomUUID();
const instances = stackTraceInstances();
toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`));
console.warn({
id,
debugId: 6864,
stack: instances
});
}
}
return children;
};
function onBeforeLeave(el) {
el.style.top = \`\${el.offsetTop}px\`;
el.style.left = \`\${el.offsetLeft}px\`;
}
function onLeaveCanceled(el) {
el.style.top = "";
el.style.left = "";
}
return () => h(defaultStore.state.animation ? TransitionGroup : "div", {
class: {
[$style["date-separated-list"]]: true,
[$style["date-separated-list-nogap"]]: props.noGap,
[$style["reversed"]]: props.reversed,
[$style["direction-down"]]: props.direction === "down",
[$style["direction-up"]]: props.direction === "up"
},
...defaultStore.state.animation ? {
name: "list",
tag: "div",
onBeforeLeave,
onLeaveCanceled
} : {}
}, {
default: renderChildren
});
}
});
const reversed = "xxiZh";
const separator = "xxeDx";
const date = "xxawD";
const style0 = {
"date-separated-list": "xfKPa",
"date-separated-list-nogap": "xf9zr",
"direction-up": "x7AeO",
"direction-down": "xBIqc",
reversed: reversed,
separator: separator,
date: date,
"date-1": "xwtmh",
"date-1-icon": "xsNPa",
"date-2": "x1xvw",
"date-2-icon": "x9ZiG"
};
const cssModules = {
"$style": style0
};
const MkDateSeparatedList = _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
export {MkDateSeparatedList as M};
`.slice(1));
});

View File

@@ -0,0 +1,275 @@
import { generate } from 'astring';
import * as estree from 'estree';
import { walk } from '../node_modules/estree-walker/src/index.js';
import type * as estreeWalker from 'estree-walker';
import type { Plugin } from 'vite';
function isFalsyIdentifier(identifier: estree.Identifier): boolean {
return identifier.name === 'undefined' || identifier.name === 'NaN';
}
function normalizeClassWalker(tree: estree.Node): string | null {
if (tree.type === 'Identifier') return isFalsyIdentifier(tree) ? '' : null;
if (tree.type === 'Literal') return typeof tree.value === 'string' ? tree.value : '';
if (tree.type === 'BinaryExpression') {
if (tree.operator !== '+') return null;
const left = normalizeClassWalker(tree.left);
const right = normalizeClassWalker(tree.right);
if (left === null || right === null) return null;
return `${left}${right}`;
}
if (tree.type === 'TemplateLiteral') {
if (tree.expressions.some((x) => x.type !== 'Literal' && (x.type !== 'Identifier' || !isFalsyIdentifier(x)))) return null;
return tree.quasis.reduce((a, c, i) => {
const v = i === tree.quasis.length - 1 ? '' : (tree.expressions[i] as Partial<estree.Literal>).value;
return a + c.value.raw + (typeof v === 'string' ? v : '');
}, '');
}
if (tree.type === 'ArrayExpression') {
const values = tree.elements.map((treeNode) => {
if (treeNode === null) return '';
if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument);
return normalizeClassWalker(treeNode);
});
if (values.some((x) => x === null)) return null;
return values.join(' ');
}
if (tree.type === 'ObjectExpression') {
const values = tree.properties.map((treeNode) => {
if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument);
let x = treeNode.value;
let inveted = false;
while (x.type === 'UnaryExpression' && x.operator === '!') {
x = x.argument;
inveted = !inveted;
}
if (x.type === 'Literal') {
if (inveted === !x.value) {
return treeNode.key.type === 'Identifier' ? treeNode.computed ? null : treeNode.key.name : treeNode.key.type === 'Literal' ? treeNode.key.value : '';
} else {
return '';
}
}
if (x.type === 'Identifier') {
if (inveted !== isFalsyIdentifier(x)) {
return '';
} else {
return null;
}
}
return null;
});
if (values.some((x) => x === null)) return null;
return values.join(' ');
}
console.error(`Unexpected node type: ${tree.type}`);
return null;
}
export function normalizeClass(tree: estree.Node): string | null {
const walked = normalizeClassWalker(tree);
return walked && walked.replace(/^\s+|\s+(?=\s)|\s+$/g, '');
}
export function unwindCssModuleClassName(ast: estree.Node): void {
(walk as typeof estreeWalker.walk)(ast, {
enter(node, parent): void {
if (parent?.type !== 'Program') return;
if (node.type !== 'VariableDeclaration') return;
if (node.declarations.length !== 1) return;
if (node.declarations[0].id.type !== 'Identifier') return;
const name = node.declarations[0].id.name;
if (node.declarations[0].init?.type !== 'CallExpression') return;
if (node.declarations[0].init.callee.type !== 'Identifier') return;
if (node.declarations[0].init.callee.name !== '_export_sfc') return;
if (node.declarations[0].init.arguments.length !== 2) return;
if (node.declarations[0].init.arguments[0].type !== 'Identifier') return;
const ident = node.declarations[0].init.arguments[0].name;
if (!ident.startsWith('_sfc_main')) return;
if (node.declarations[0].init.arguments[1].type !== 'ArrayExpression') return;
if (node.declarations[0].init.arguments[1].elements.length === 0) return;
const __cssModulesIndex = node.declarations[0].init.arguments[1].elements.findIndex((x) => {
if (x?.type !== 'ArrayExpression') return false;
if (x.elements.length !== 2) return false;
if (x.elements[0]?.type !== 'Literal') return false;
if (x.elements[0].value !== '__cssModules') return false;
if (x.elements[1]?.type !== 'Identifier') return false;
return true;
});
if (!~__cssModulesIndex) return;
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) => {
if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false;
if (x.declarations[0].id.type !== 'Identifier') return false;
if (x.declarations[0].id.name !== cssModuleForestName) return false;
if (x.declarations[0].init?.type !== 'ObjectExpression') return false;
return true;
}) as unknown as estree.VariableDeclaration;
const moduleForest = new Map((cssModuleForestNode.declarations[0].init as estree.ObjectExpression).properties.flatMap((property) => {
if (property.type !== 'Property') return [];
if (property.key.type !== 'Literal') return [];
if (property.value.type !== 'Identifier') return [];
return [[property.key.value as string, property.value.name as string]];
}));
const sfcMain = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false;
if (x.declarations[0].id.type !== 'Identifier') return false;
if (x.declarations[0].id.name !== ident) return false;
return true;
}) as unknown as estree.VariableDeclaration;
if (sfcMain.declarations[0].init?.type !== 'CallExpression') return;
if (sfcMain.declarations[0].init.callee.type !== 'Identifier') return;
if (sfcMain.declarations[0].init.callee.name !== 'defineComponent') return;
if (sfcMain.declarations[0].init.arguments.length !== 1) return;
if (sfcMain.declarations[0].init.arguments[0].type !== 'ObjectExpression') return;
const setup = sfcMain.declarations[0].init.arguments[0].properties.find((x) => {
if (x.type !== 'Property') return false;
if (x.key.type !== 'Identifier') return false;
if (x.key.name !== 'setup') return false;
return true;
}) as unknown as estree.Property;
if (setup.value.type !== 'FunctionExpression') return;
const render = setup.value.body.body.find((x) => {
if (x.type !== 'ReturnStatement') return false;
return true;
}) as unknown as estree.ReturnStatement;
if (render.argument?.type !== 'ArrowFunctionExpression') return;
if (render.argument.params.length !== 2) return;
const ctx = render.argument.params[0];
if (ctx.type !== 'Identifier') return;
if (ctx.name !== '_ctx') return;
if (render.argument.body.type !== 'BlockStatement') return;
for (const [key, value] of moduleForest) {
const cssModuleTreeNode = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false;
if (x.declarations[0].id.type !== 'Identifier') return false;
if (x.declarations[0].id.name !== value) return false;
return true;
}) as unknown as estree.VariableDeclaration;
if (cssModuleTreeNode.declarations[0].init?.type !== 'ObjectExpression') return;
const moduleTree = new Map(cssModuleTreeNode.declarations[0].init.properties.flatMap((property) => {
if (property.type !== 'Property') return [];
const actualKey = property.key.type === 'Identifier' ? property.key.name : property.key.type === 'Literal' ? property.key.value : null;
if (typeof actualKey !== 'string') return [];
if (property.value.type === 'Literal') return [[actualKey, property.value.value as string]];
if (property.value.type !== 'Identifier') return [];
const labelledValue = property.value.name;
const actualValue = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false;
if (x.declarations[0].id.type !== 'Identifier') return false;
if (x.declarations[0].id.name !== labelledValue) return false;
return true;
}) as unknown as estree.VariableDeclaration;
if (actualValue.declarations[0].init?.type !== 'Literal') return [];
return [[actualKey, actualValue.declarations[0].init.value as string]];
}));
(walk as typeof estreeWalker.walk)(render.argument.body, {
enter(childNode) {
if (childNode.type !== 'MemberExpression') return;
if (childNode.object.type !== 'MemberExpression') return;
if (childNode.object.object.type !== 'Identifier') return;
if (childNode.object.object.name !== ctx.name) return;
if (childNode.object.property.type !== 'Identifier') return;
if (childNode.object.property.name !== key) return;
if (childNode.property.type !== 'Identifier') return;
const actualValue = moduleTree.get(childNode.property.name);
if (actualValue === undefined) return;
this.replace({
type: 'Literal',
value: actualValue,
});
},
});
(walk as typeof estreeWalker.walk)(render.argument.body, {
enter(childNode) {
if (childNode.type !== 'MemberExpression') return;
if (childNode.object.type !== 'MemberExpression') return;
if (childNode.object.object.type !== 'Identifier') return;
if (childNode.object.object.name !== ctx.name) return;
if (childNode.object.property.type !== 'Identifier') return;
if (childNode.object.property.name !== key) return;
if (childNode.property.type !== 'Identifier') return;
console.error(`Undefined style detected: ${key}.${childNode.property.name} (in ${name})`);
this.replace({
type: 'Identifier',
name: 'undefined',
});
},
});
(walk as typeof estreeWalker.walk)(render.argument.body, {
enter(childNode) {
if (childNode.type !== 'CallExpression') return;
if (childNode.callee.type !== 'Identifier') return;
if (childNode.callee.name !== 'normalizeClass') return;
if (childNode.arguments.length !== 1) return;
const normalized = normalizeClass(childNode.arguments[0]);
if (normalized === null) return;
this.replace({
type: 'Literal',
value: normalized,
});
},
});
}
if (node.declarations[0].init.arguments[1].elements.length === 1) {
this.replace({
type: 'VariableDeclaration',
declarations: [{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: node.declarations[0].id.name,
},
init: {
type: 'Identifier',
name: ident,
},
}],
kind: 'const',
});
} else {
this.replace({
type: 'VariableDeclaration',
declarations: [{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: node.declarations[0].id.name,
},
init: {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: '_export_sfc',
},
arguments: [{
type: 'Identifier',
name: ident,
}, {
type: 'ArrayExpression',
elements: node.declarations[0].init.arguments[1].elements.slice(0, __cssModulesIndex).concat(node.declarations[0].init.arguments[1].elements.slice(__cssModulesIndex + 1)),
}],
},
}],
kind: 'const',
});
}
},
});
}
// eslint-disable-next-line import/no-default-export
export default function pluginUnwindCssModuleClassName(): Plugin {
return {
name: 'UnwindCssModuleClassName',
renderChunk(code): { code: string } {
const ast = this.parse(code) as unknown as estree.Node;
unwindCssModuleClassName(ast);
return { code: generate(ast) };
},
};
}

View File

@@ -20,12 +20,13 @@
"@rollup/plugin-replace": "5.0.2",
"@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.13.3",
"@tabler/icons-webfont": "2.17.0",
"@tabler/icons-webfont": "2.21.0",
"@vitejs/plugin-vue": "4.2.3",
"@vue-macros/reactivity-transform": "0.3.8",
"@vue-macros/reactivity-transform": "0.3.9",
"@vue/compiler-sfc": "3.3.4",
"astring": "1.8.6",
"autosize": "6.0.1",
"broadcast-channel": "4.20.2",
"broadcast-channel": "5.1.0",
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"buraha": "github:misskey-dev/buraha",
"canvas-confetti": "1.6.0",
@@ -34,11 +35,12 @@
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1",
"chromatic": "6.17.4",
"chromatic": "6.18.0",
"compare-versions": "5.0.3",
"cropperjs": "2.0.0-beta.2",
"date-fns": "2.30.0",
"escape-regexp": "0.0.1",
"estree-walker": "^3.0.3",
"eventemitter3": "5.0.1",
"gsap": "3.11.5",
"idb-keyval": "6.2.1",
@@ -61,13 +63,13 @@
"strict-event-emitter-types": "2.0.0",
"syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0",
"three": "0.151.3",
"three": "0.153.0",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.6",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typescript": "5.0.4",
"typescript": "5.1.3",
"uuid": "9.0.0",
"vanilla-tilt": "1.8.0",
"vite": "4.3.9",
@@ -112,19 +114,19 @@
"@types/uuid": "9.0.1",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5",
"@vitest/coverage-c8": "0.31.1",
"@typescript-eslint/eslint-plugin": "5.59.8",
"@typescript-eslint/parser": "5.59.8",
"@vitest/coverage-c8": "0.31.4",
"@vue/runtime-core": "3.3.4",
"astring": "1.8.5",
"acorn": "^8.8.2",
"chokidar-cli": "3.0.0",
"cross-env": "7.0.3",
"cypress": "12.13.0",
"eslint": "8.40.0",
"eslint": "8.41.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-vue": "9.14.1",
"fast-glob": "3.2.12",
"happy-dom": "9.19.2",
"happy-dom": "9.20.3",
"micromatch": "3.1.10",
"msw": "1.2.1",
"msw-storybook-addon": "1.8.0",
@@ -136,7 +138,7 @@
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"summaly": "github:misskey-dev/summaly",
"vite-plugin-turbosnap": "1.0.2",
"vitest": "0.31.1",
"vitest": "0.31.4",
"vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.3.0",
"vue-tsc": "1.6.5"

View File

@@ -3,7 +3,14 @@
<div v-if="achievements" :class="$style.root">
<div v-for="achievement in achievements" :key="achievement" :class="$style.achievement" class="_panel">
<div :class="$style.icon">
<div :class="[$style.iconFrame, $style['iconFrame_' + ACHIEVEMENT_BADGES[achievement.name].frame]]">
<div
:class="[$style.iconFrame, {
[$style.iconFrame_bronze]: ACHIEVEMENT_BADGES[achievement.name].frame === 'bronze',
[$style.iconFrame_silver]: ACHIEVEMENT_BADGES[achievement.name].frame === 'silver',
[$style.iconFrame_gold]: ACHIEVEMENT_BADGES[achievement.name].frame === 'gold',
[$style.iconFrame_platinum]: ACHIEVEMENT_BADGES[achievement.name].frame === 'platinum',
}]"
>
<div :class="[$style.iconInner]" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }">
<img :class="$style.iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img">
</div>

View File

@@ -10,7 +10,7 @@
</li>
<li tabindex="-1" :class="$style.item" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li>
</ol>
<ol v-else-if="hashtags.length > 0" ref="suggests" :class="[$style.list, $style.hashtags]">
<ol v-else-if="hashtags.length > 0" ref="suggests" :class="$style.list">
<li v-for="hashtag in hashtags" tabindex="-1" :class="$style.item" @click="complete(type, hashtag)" @keydown="onKeydown">
<span class="name">{{ hashtag }}</span>
</li>
@@ -42,7 +42,7 @@ import { acct } from '@/filters/user';
import * as os from '@/os';
import { MFM_TAGS } from '@/scripts/mfm-tags';
import { defaultStore } from '@/store';
import { emojilist } from '@/scripts/emojilist';
import { emojilist, getEmojiName } from '@/scripts/emojilist';
import { i18n } from '@/i18n';
import { miLocalStorage } from '@/local-storage';
import { customEmojis } from '@/custom-emojis';
@@ -71,6 +71,19 @@ const emojiDb = computed(() => {
url: char2path(x.char),
}));
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const [emoji, keywords] of Object.entries(index)) {
for (const k of keywords) {
unicodeEmojiDB.push({
emoji: emoji,
name: k,
aliasOf: getEmojiName(emoji)!,
url: char2path(emoji),
});
}
}
}
unicodeEmojiDB.sort((a, b) => a.name.length - b.name.length);
//#endregion

View File

@@ -7,7 +7,7 @@
@click="emit('click', $event)"
@mousedown="onMousedown"
>
<div ref="ripples" :class="$style.ripples"></div>
<div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
<div :class="$style.content">
<slot></slot>
</div>
@@ -18,7 +18,7 @@
:to="to"
@mousedown="onMousedown"
>
<div ref="ripples" :class="$style.ripples"></div>
<div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
<div :class="$style.content">
<slot></slot>
</div>
@@ -26,9 +26,7 @@
</template>
<script lang="ts" setup>
import { nextTick, onMounted, useCssModule } from 'vue';
const $style = useCssModule();
import { nextTick, onMounted } from 'vue';
const props = defineProps<{
type?: 'button' | 'submit' | 'reset';
@@ -81,7 +79,7 @@ function onMousedown(evt: MouseEvent): void {
const rect = target.getBoundingClientRect();
const ripple = document.createElement('div');
ripple.classList.add($style.ripple);
ripple.classList.add(ripples!.dataset.childrenClass!);
ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';

View File

@@ -3,7 +3,7 @@
<div v-if="game.ready" :class="$style.game">
<div :class="$style.cps" class="">{{ number(cps) }}cps</div>
<div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
<button v-click-anime class="_button" :class="$style.button" @click="onClick">
<button v-click-anime class="_button" @click="onClick">
<img src="/client-assets/cookie.png" :class="$style.img">
</button>
</div>
@@ -84,10 +84,6 @@ onUnmounted(() => {
margin-bottom: 6px;
}
.button {
}
.img {
max-width: 90px;
}

View File

@@ -1,5 +1,5 @@
<template>
<div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.hideHeader]: !showHeader, [$style.scrollable]: scrollable, [$style.closed]: !showBody }]">
<div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.scrollable]: scrollable }]">
<header v-if="showHeader" ref="headerEl" :class="$style.header">
<div :class="$style.title">
<span :class="$style.titleIcon"><slot name="icon"></slot></span>
@@ -34,7 +34,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref, shallowRef, watch } from 'vue';
import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
@@ -83,13 +83,19 @@ function afterLeave(el) {
const calcOmit = () => {
if (omitted.value || ignoreOmit.value || props.maxHeight == null) return;
if (!contentEl.value) return;
const height = contentEl.value.offsetHeight;
omitted.value = height > props.maxHeight;
};
const omitObserver = new ResizeObserver((entries, observer) => {
calcOmit();
});
onMounted(() => {
watch(showBody, v => {
const headerHeight = props.showHeader ? headerEl.value.offsetHeight : 0;
if (!rootEl.value) return;
const headerHeight = props.showHeader ? headerEl.value?.offsetHeight ?? 0 : 0;
rootEl.value.style.minHeight = `${headerHeight}px`;
if (v) {
rootEl.value.style.flexBasis = 'auto';
@@ -100,13 +106,15 @@ onMounted(() => {
immediate: true,
});
rootEl.value.style.setProperty('--maxHeight', props.maxHeight + 'px');
if (rootEl.value) rootEl.value.style.setProperty('--maxHeight', props.maxHeight + 'px');
calcOmit();
new ResizeObserver((entries, observer) => {
calcOmit();
}).observe(contentEl.value);
if (contentEl.value) omitObserver.observe(contentEl.value);
});
onUnmounted(() => {
omitObserver.disconnect();
});
</script>

View File

@@ -36,7 +36,7 @@ export default defineComponent({
},
setup(props, { slots, expose }) {
const $style = useCssModule();
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
function getDateText(time: string) {
const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1;

View File

@@ -4,7 +4,15 @@
<div v-if="icon" :class="$style.icon">
<i :class="icon"></i>
</div>
<div v-else-if="!input && !select" :class="[$style.icon, $style['type_' + type]]">
<div
v-else-if="!input && !select"
:class="[$style.icon, {
[$style.type_success]: type === 'success',
[$style.type_error]: type === 'error',
[$style.type_warning]: type === 'warning',
[$style.type_info]: type === 'info',
}]"
>
<i v-if="type === 'success'" :class="$style.iconInner" class="ti ti-check"></i>
<i v-else-if="type === 'error'" :class="$style.iconInner" class="ti ti-circle-x"></i>
<i v-else-if="type === 'warning'" :class="$style.iconInner" class="ti ti-alert-triangle"></i>

View File

@@ -1,7 +1,8 @@
<template>
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
<div ref="emojisEl" class="emojis">
<!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
<div ref="emojisEl" class="emojis" tabindex="-1">
<section class="result">
<div v-if="searchResultCustom.length > 0" class="body">
<button
@@ -101,7 +102,7 @@ import { isTouchUsing } from '@/scripts/touch';
import { deviceKind } from '@/scripts/device-kind';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
import { customEmojiCategories, customEmojis } from '@/custom-emojis';
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis';
import { $i } from '@/account';
const props = withDefaults(defineProps<{
@@ -223,7 +224,6 @@ watch(q, () => {
if (newQ.includes(' ')) { // AND検索
const keywords = newQ.split(' ');
// 名前にキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
@@ -232,11 +232,12 @@ watch(q, () => {
}
if (matches.size >= max) return matches;
// 名前にキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
if (matches.size >= max) break;
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
} else {
@@ -248,6 +249,15 @@ watch(q, () => {
}
if (matches.size >= max) return matches;
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (index[emoji.char].some(k => k.startsWith(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
for (const emoji of emojis) {
if (emoji.name.includes(newQ)) {
matches.add(emoji);
@@ -255,6 +265,15 @@ watch(q, () => {
}
}
if (matches.size >= max) return matches;
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (index[emoji.char].some(k => k.includes(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
}
return matches;
@@ -337,7 +356,7 @@ function done(query?: string): boolean | void {
if (query == null || typeof query !== 'string') return;
const q2 = query.replace(/:/g, '');
const exactMatchCustom = customEmojis.value.find(emoji => emoji.name === q2);
const exactMatchCustom = customEmojisMap.get(q2);
if (exactMatchCustom) {
chosen(exactMatchCustom);
return true;

View File

@@ -5,7 +5,7 @@
<div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
<div :class="$style.headerIcon"><slot name="icon"></slot></div>
<div :class="$style.headerText">
<div :class="$style.headerTextMain">
<div>
<MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine>
</div>
<div :class="$style.headerTextSub">
@@ -185,10 +185,6 @@ onMounted(() => {
padding-right: 12px;
}
.headerTextMain {
}
.headerTextSub {
color: var(--fgTransparentWeak);
font-size: .85em;

View File

@@ -131,8 +131,7 @@ onBeforeUnmount(() => {
position: relative;
display: inline-block;
font-weight: bold;
color: var(--accent);
background: transparent;
color: var(--fgOnWhite);
border: solid 1px var(--accent);
padding: 0;
height: 31px;

View File

@@ -1,9 +1,9 @@
<template>
<div ref="root" :class="[$style.root, { [$style.cover]: cover }]" :title="title ?? ''">
<div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''">
<TransitionGroup
:duration="defaultStore.state.animation && props.transition?.duration || undefined"
:enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined"
:leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style['transition_leaveActive']) || undefined"
:leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined"
:enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
:leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
@@ -23,6 +23,11 @@ import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => {
// テスト環境で Web Worker インスタンスは作成できない
if (import.meta.env.MODE === 'test') {
resolve(null);
return;
}
const testWorker = new TestWebGL2();
testWorker.addEventListener('message', event => {
if (event.data.result) {
@@ -42,11 +47,10 @@ const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => {
</script>
<script lang="ts" setup>
import { computed, nextTick, onMounted, onUnmounted, shallowRef, useCssModule, watch } from 'vue';
import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
import { v4 as uuid } from 'uuid';
import { render } from 'buraha';
import { defaultStore } from '@/store';
const $style = useCssModule();
const props = withDefaults(defineProps<{
transition?: {

View File

@@ -32,7 +32,7 @@
<div v-if="image.comment" :class="$style.indicator">ALT</div>
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
</div>
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots"></i></button>
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
</template>
</div>
</template>
@@ -87,6 +87,7 @@ function showMenu(ev: MouseEvent) {
}, ...(iAmModerator ? [{
text: i18n.ts.markAsSensitive,
icon: 'ti ti-eye-exclamation',
danger: true,
action: () => {
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
},
@@ -130,13 +131,14 @@ function showMenu(ev: MouseEvent) {
.menu {
display: block;
position: absolute;
border-radius: 6px;
border-radius: 999px;
background-color: rgba(0, 0, 0, 0.3);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
color: #fff;
font-size: 0.8em;
padding: 6px 8px;
width: 32px;
height: 32px;
text-align: center;
bottom: 10px;
right: 10px;

View File

@@ -6,8 +6,11 @@
ref="gallery"
:class="[
$style.medias,
count <= 4 ? $style['n' + count] : $style.nMany,
$style[`n1${defaultStore.reactiveState.mediaListWithOneImageAppearance.value}`]
count === 1 ? [$style.n1, {
[$style.n116_9]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '16_9',
[$style.n11_1]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '1_1',
[$style.n12_3]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '2_3',
}] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany,
]"
>
<template v-for="media in mediaList.filter(media => previewable(media))">
@@ -20,7 +23,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref, useCssModule, watch, shallowRef } from 'vue';
import { onMounted, watch, shallowRef } from 'vue';
import * as misskey from 'misskey-js';
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe';
@@ -37,8 +40,6 @@ const props = defineProps<{
raw?: boolean;
}>();
const $style = useCssModule();
const gallery = shallowRef<HTMLDivElement>();
const pswpZIndex = os.claimZIndex('middle');
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
@@ -96,7 +97,7 @@ onMounted(() => {
return item;
}),
gallery: gallery.value,
mainClass: $style.pswp,
mainClass: 'pswp',
children: '.image',
thumbSelector: '.image',
loop: false,
@@ -268,7 +269,7 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
border-radius: 8px;
}
.pswp {
:global(.pswp) {
--pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important;
--pswp-bg: var(--modalBg) !important;
}

View File

@@ -2,7 +2,7 @@
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }">
<img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt="">
<span>
<span :class="$style.username">@{{ username }}</span>
<span>@{{ username }}</span>
<span v-if="(host != localHost) || defaultStore.state.showFullAcct" :class="$style.host">@{{ toUnicode(host) }}</span>
</span>
</MkA>

View File

@@ -49,7 +49,7 @@
<span>{{ i18n.ts.none }}</span>
</span>
</div>
<div v-if="childMenu" :class="$style.child">
<div v-if="childMenu">
<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned"/>
</div>
</div>

View File

@@ -1,10 +1,30 @@
<template>
<Transition
:name="transitionName"
:enterActiveClass="$style['transition_' + transitionName + '_enterActive']"
:leaveActiveClass="$style['transition_' + transitionName + '_leaveActive']"
:enterFromClass="$style['transition_' + transitionName + '_enterFrom']"
:leaveToClass="$style['transition_' + transitionName + '_leaveTo']"
:enterActiveClass="normalizeClass({
[$style.transition_modalDrawer_enterActive]: transitionName === 'modal-drawer',
[$style.transition_modalPopup_enterActive]: transitionName === 'modal-popup',
[$style.transition_modal_enterActive]: transitionName === 'modal',
[$style.transition_send_enterActive]: transitionName === 'send',
})"
:leaveActiveClass="normalizeClass({
[$style.transition_modalDrawer_leaveActive]: transitionName === 'modal-drawer',
[$style.transition_modalPopup_leaveActive]: transitionName === 'modal-popup',
[$style.transition_modal_leaveActive]: transitionName === 'modal',
[$style.transition_send_leaveActive]: transitionName === 'send',
})"
:enterFromClass="normalizeClass({
[$style.transition_modalDrawer_enterFrom]: transitionName === 'modal-drawer',
[$style.transition_modalPopup_enterFrom]: transitionName === 'modal-popup',
[$style.transition_modal_enterFrom]: transitionName === 'modal',
[$style.transition_send_enterFrom]: transitionName === 'send',
})"
:leaveToClass="normalizeClass({
[$style.transition_modalDrawer_leaveTo]: transitionName === 'modal-drawer',
[$style.transition_modalPopup_leaveTo]: transitionName === 'modal-popup',
[$style.transition_modal_leaveTo]: transitionName === 'modal',
[$style.transition_send_leaveTo]: transitionName === 'send',
})"
:duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened"
>
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
@@ -17,7 +37,7 @@
</template>
<script lang="ts" setup>
import { nextTick, onMounted, watch, provide } from 'vue';
import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch } from 'vue';
import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch';
import { defaultStore } from '@/store';
@@ -38,7 +58,7 @@ type ModalTypes = 'popup' | 'dialog' | 'drawer';
const props = withDefaults(defineProps<{
manualShowing?: boolean | null;
anchor?: { x: string; y: string; };
src?: HTMLElement;
src?: HTMLElement | null;
preferType?: ModalTypes | 'auto';
zPriority?: 'low' | 'middle' | 'high';
noOverlap?: boolean;
@@ -264,6 +284,10 @@ const onOpened = () => {
}, { passive: true });
};
const alignObserver = new ResizeObserver((entries, observer) => {
align();
});
onMounted(() => {
watch(() => props.src, async () => {
if (props.src) {
@@ -278,12 +302,14 @@ onMounted(() => {
}, { immediate: true });
nextTick(() => {
new ResizeObserver((entries, observer) => {
align();
}).observe(content!);
alignObserver.observe(content!);
});
});
onUnmounted(() => {
alignObserver.disconnect();
});
defineExpose({
close,
});
@@ -339,8 +365,8 @@ defineExpose({
}
}
.transition_modal-popup_enterActive,
.transition_modal-popup_leaveActive {
.transition_modalPopup_enterActive,
.transition_modalPopup_leaveActive {
> .bg {
transition: opacity 0.1s !important;
}
@@ -350,8 +376,8 @@ defineExpose({
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important;
}
}
.transition_modal-popup_enterFrom,
.transition_modal-popup_leaveTo {
.transition_modalPopup_enterFrom,
.transition_modalPopup_leaveTo {
> .bg {
opacity: 0;
}
@@ -364,7 +390,7 @@ defineExpose({
}
}
.transition_modal-drawer_enterActive {
.transition_modalDrawer_enterActive {
> .bg {
transition: opacity 0.2s !important;
}
@@ -373,7 +399,7 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
}
}
.transition_modal-drawer_leaveActive {
.transition_modalDrawer_leaveActive {
> .bg {
transition: opacity 0.2s !important;
}
@@ -382,8 +408,8 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
}
}
.transition_modal-drawer_enterFrom,
.transition_modal-drawer_leaveTo {
.transition_modalDrawer_enterFrom,
.transition_modalDrawer_leaveTo {
> .bg {
opacity: 0;
}

View File

@@ -44,8 +44,8 @@
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/>
<div :class="$style.main">
<MkNoteHeader :class="$style.header" :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" :class="$style.ticker" :instance="appearNote.user.instance"/>
<MkNoteHeader :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
<div style="container-type: inline-size;">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/>
@@ -58,13 +58,13 @@
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
<div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/>
<div v-else :class="$style.translated">
<div v-else>
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
</div>
</div>
</div>
<div v-if="appearNote.files.length > 0" :class="$style.files">
<div v-if="appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/>
</div>
<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/>
@@ -205,8 +205,11 @@ const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false);
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
const isLong = (appearNote.cw == null && appearNote.text != null && (
(appearNote.text.includes('$[x2')) ||
(appearNote.text.includes('$[x3')) ||
(appearNote.text.includes('$[x4')) ||
(appearNote.text.includes('$[scale')) ||
(appearNote.text.includes('$[position')) ||
(appearNote.text.split('\n').length > 9) ||
(appearNote.text.length > 500) ||
(appearNote.files.length >= 5) ||
@@ -274,7 +277,7 @@ function renote(viaKeyboard = false) {
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
os.api('notes/create', {
renoteId: appearNote.id,
channelId: appearNote.channelId,
@@ -305,7 +308,7 @@ function renote(viaKeyboard = false) {
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
os.api('notes/create', {
renoteId: appearNote.id,
}).then(() => {
@@ -379,6 +382,8 @@ function undoReact(note): void {
function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
// 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
if (el.tagName === 'AUDIO') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}

View File

@@ -4,7 +4,7 @@
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
:class="[$style.root, { [$style.renote]: isRenote }]"
:class="$style.root"
>
<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
@@ -52,7 +52,7 @@
</div>
</div>
<div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div>
<MkInstanceTicker v-if="showTicker" :class="$style.ticker" :instance="appearNote.user.instance"/>
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
</div>
</header>
<div :class="$style.noteContent">
@@ -72,7 +72,7 @@
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
</div>
</div>
<div v-if="appearNote.files.length > 0" :class="$style.files">
<div v-if="appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/>
</div>
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/>

View File

@@ -6,7 +6,7 @@
<MkUserName :user="$i" :nowrap="true"/>
</div>
<div>
<div :class="$style.content">
<div>
<Mfm :text="text.trim()" :author="$i" :i="$i"/>
</div>
</div>

View File

@@ -5,7 +5,19 @@
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
<img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/>
<div :class="[$style.subIcon, $style['t_' + notification.type]]">
<div
:class="[$style.subIcon, {
[$style.t_follow]: notification.type === 'follow',
[$style.t_followRequestAccepted]: notification.type === 'followRequestAccepted',
[$style.t_receiveFollowRequest]: notification.type === 'receiveFollowRequest',
[$style.t_renote]: notification.type === 'renote',
[$style.t_reply]: notification.type === 'reply',
[$style.t_mention]: notification.type === 'mention',
[$style.t_quote]: notification.type === 'quote',
[$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
}]"
>
<i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i>
<i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i>
@@ -34,7 +46,7 @@
<span v-else>{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
</header>
<div :class="$style.content">
<div>
<MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
@@ -243,9 +255,6 @@ useTooltip(reactionRef, (showing) => {
font-size: 0.9em;
}
.content {
}
.text {
display: flex;
width: 100%;

View File

@@ -8,7 +8,7 @@
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { onMounted, onUnmounted } from 'vue';
import { i18n } from '@/i18n';
const props = withDefaults(defineProps<{
@@ -21,16 +21,22 @@ let content = $shallowRef<HTMLElement>();
let omitted = $ref(false);
let ignoreOmit = $ref(false);
onMounted(() => {
const calcOmit = () => {
if (omitted || ignoreOmit) return;
omitted = content.offsetHeight > props.maxHeight;
};
const calcOmit = () => {
if (omitted || ignoreOmit) return;
omitted = content.offsetHeight > props.maxHeight;
};
const omitObserver = new ResizeObserver((entries, observer) => {
calcOmit();
new ResizeObserver((entries, observer) => {
calcOmit();
}).observe(content);
});
onMounted(() => {
calcOmit();
omitObserver.observe(content);
});
onUnmounted(() => {
omitObserver.disconnect();
});
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div :class="{ [$style.done]: closed || isVoted }">
<ul :class="$style.choices">
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="[$style.choice, { [$style.voted]: choice.voted }]" @click="vote(i)">
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span :class="$style.fg">
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>

View File

@@ -27,16 +27,16 @@
<span :class="$style.headerRightButtonText">{{ channel.name }}</span>
</button>
</template>
<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, $style.localOnly, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly">
<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly">
<span v-if="!localOnly"><i class="ti ti-rocket"></i></span>
<span v-else><i class="ti ti-rocket-off"></i></span>
</button>
<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, $style.reactionAcceptance, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance">
<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance">
<span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span>
<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
<span v-else><i class="ti ti-icons"></i></span>
</button>
<button v-click-anime class="_button" :class="[$style.submit, { [$style.submitPosting]: posting }]" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
<button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
<div :class="$style.submitInner">
<template v-if="posted"></template>
<template v-else-if="posting"><MkEllipsis/></template>
@@ -66,7 +66,7 @@
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
</div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
<XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
<div v-if="showingOptions" style="padding: 8px 16px;">

View File

@@ -1,6 +1,6 @@
<template>
<div :class="[$style.root, { [$style.collapsed]: collapsed }]">
<div :class="$style.body">
<div>
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
@@ -76,10 +76,6 @@ const collapsed = $ref(
}
}
.body {
}
.reply {
margin-right: 6px;
color: var(--accent);

View File

@@ -6,7 +6,7 @@
ref="inputEl"
v-model="v"
v-adaptive-border
:class="[$style.textarea, { [$style.code]: code, _monospace: code }]"
:class="[$style.textarea, { _monospace: code }]"
:disabled="disabled"
:required="required"
:readonly="readonly"

View File

@@ -22,7 +22,7 @@
</div>
</template>
<template v-else-if="tweetId && tweetExpanded">
<div ref="twitter" :class="$style.twitter">
<div ref="twitter">
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
</div>
<div :class="$style.action">
@@ -31,7 +31,7 @@
</MkButton>
</div>
</template>
<div v-else :class="$style.urlPreview">
<div v-else>
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`">
</div>
@@ -210,13 +210,6 @@ onUnmounted(() => {
width: 100%;
}
.twitter {
}
.urlPreview {
}
.link {
position: relative;
display: block;

View File

@@ -8,7 +8,7 @@
</div>
<span v-if="$i && $i.id !== user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
<div :class="$style.description">
<div v-if="user.description" class="mfm">
<div v-if="user.description" :class="$style.mfm">
<Mfm :text="user.description" :author="user" :i="$i"/>
</div>
<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>

View File

@@ -1,5 +1,13 @@
<template>
<div v-tooltip="text" :class="[$style.root, $style['status_' + user.onlineStatus]]"></div>
<div
v-tooltip="text"
:class="[$style.root, {
[$style.status_online]: user.onlineStatus === 'online',
[$style.status_active]: user.onlineStatus === 'active',
[$style.status_offline]: user.onlineStatus === 'offline',
[$style.status_unknown]: user.onlineStatus === 'unknown',
}]"
></div>
</template>
<script lang="ts" setup>

View File

@@ -22,7 +22,7 @@
<div :class="$style.username"><MkAcct :user="user"/></div>
</div>
<div :class="$style.description">
<Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/>
<Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user" :i="$i"/>
<div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div>
</div>
<div :class="$style.status">
@@ -192,6 +192,13 @@ onMounted(() => {
border-bottom: solid 1px var(--divider);
}
.mfm {
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
.status {
padding: 16px 26px 16px 26px;
}

View File

@@ -9,7 +9,7 @@
@closed="$emit('closed')"
>
<template #header>{{ i18n.ts.selectUser }}</template>
<div :class="$style.root">
<div>
<div :class="$style.form">
<FormSplit :minWidth="170">
<MkInput v-model="username" :autofocus="true" @update:modelValue="search">
@@ -126,8 +126,6 @@ onMounted(() => {
</script>
<style lang="scss" module>
.root {
}
.form {
padding: 0 var(--root-margin);

View File

@@ -3,9 +3,9 @@
<div :class="$style.root">
<div v-for="u in users" :key="u.id" :class="$style.user">
<MkAvatar :class="$style.avatar" :user="u"/>
<MkUserName :class="$style.name" :user="u" :nowrap="true"/>
<MkUserName :user="u" :nowrap="true"/>
</div>
<div v-if="users.length < count" :class="$style.omitted">+{{ count - users.length }}</div>
<div v-if="users.length < count">+{{ count - users.length }}</div>
</div>
</MkTooltip>
</template>
@@ -43,14 +43,6 @@ const emit = defineEmits<{
}
}
.name {
}
.omitted {
}
.avatar {
width: 24px;
height: 24px;

View File

@@ -39,7 +39,7 @@
<MkTimeline src="local"/>
</div>
</div>
<div :class="[$style.activeUsersChart, $style.panel]">
<div :class="$style.panel">
<XActiveUsersChart/>
</div>
</div>
@@ -220,8 +220,4 @@ function exploreOtherServers() {
height: 350px;
overflow: auto;
}
.activeUsersChart {
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div :class="$style.root">
<template v-if="edit">
<header :class="$style['edit-header']">
<header :class="$style.editHeader">
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select>
<template #label>{{ i18n.ts.selectWidget }}</template>
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option>
@@ -15,15 +15,15 @@
handle=".handle"
:animation="150"
:group="{ name: 'SortableMkWidgets' }"
:class="$style['edit-editing']"
:class="$style.editEditing"
@update:modelValue="v => emit('updateWidgets', v)"
>
<template #item="{element}">
<div :class="[$style.widget, $style['customize-container']]" data-cy-customize-container>
<button :class="$style['customize-container-config']" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button>
<button :class="$style['customize-container-remove']" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button>
<div :class="[$style.widget, $style.customizeContainer]" data-cy-customize-container>
<button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button>
<button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button>
<div class="handle">
<component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
<component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style.customizeContainerHandleWidget" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
</div>
</div>
</template>
@@ -130,7 +130,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
}
.edit {
&-header {
&Header {
margin: 16px 0;
> * {
@@ -139,17 +139,17 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
}
}
&-editing {
&Editing {
min-height: 100px;
}
}
.customize-container {
.customizeContainer {
position: relative;
cursor: move;
&-config,
&-remove {
&Config,
&Remove {
position: absolute;
z-index: 10000;
top: 8px;
@@ -160,17 +160,17 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
border-radius: 4px;
}
&-config {
&Config {
right: 8px + 8px + 32px;
}
&-remove {
&Remove {
right: 8px;
}
&-handle {
&Handle {
&-widget {
&Widget {
pointer-events: none;
}
}

View File

@@ -5,7 +5,7 @@
<span :class="$style.text"><slot></slot></span>
<span :class="$style.suffix">
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
<i class="ti ti-external-link" :class="$style.suffixIcon"></i>
<i class="ti ti-external-link"></i>
</span>
</a>
<MkA v-else :class="[$style.main, { [$style.active]: active }]" class="_button" :to="to" :behavior="behavior">
@@ -13,7 +13,7 @@
<span :class="$style.text"><slot></slot></span>
<span :class="$style.suffix">
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
<i class="ti ti-chevron-right" :class="$style.suffixIcon"></i>
<i class="ti ti-chevron-right"></i>
</span>
</MkA>
</div>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div :class="$style.label" @click="focus"><slot name="label"></slot></div>
<div :class="$style.content">
<div>
<slot></slot>
</div>
<div :class="$style.caption"><slot name="caption"></slot></div>

View File

@@ -1,6 +1,14 @@
<template>
<div v-if="chosen && !shouldHide" :class="$style.root">
<div v-if="!showMenu" :class="[$style.main, $style['form_' + chosen.place]]">
<div
v-if="!showMenu"
:class="[$style.main, {
[$style.form_square]: chosen.place === 'square',
[$style.form_horizontal]: chosen.place === 'horizontal',
[$style.form_horizontalBig]: chosen.place === 'horizontal-big',
[$style.form_vertical]: chosen.place === 'vertical',
}]"
>
<a :href="chosen.url" target="_blank" :class="$style.link">
<img :src="chosen.imageUrl" :class="$style.img">
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
@@ -122,7 +130,7 @@ function reduceFrequency(): void {
}
}
&.form_horizontal-big {
&.form_horizontalBig {
padding: 8px;
> .link,

View File

@@ -13,13 +13,20 @@ interface Props {
const contentSymbol = Symbol();
const observer = new ResizeObserver((entries) => {
const results: {
container: HTMLSpanElement;
transform: string;
}[] = [];
for (const entry of entries) {
const content = (entry.target[contentSymbol] ? entry.target : entry.target.firstElementChild) as HTMLSpanElement;
const props: Required<Props> = content[contentSymbol];
const container = content.parentElement as HTMLSpanElement;
const contentWidth = content.getBoundingClientRect().width;
const containerWidth = container.getBoundingClientRect().width;
container.style.transform = `scaleX(${Math.max(props.minScale, Math.min(1, containerWidth / contentWidth))})`;
results.push({ container, transform: `scaleX(${Math.max(props.minScale, Math.min(1, containerWidth / contentWidth))})` });
}
for (const result of results) {
result.container.style.transform = result.transform;
}
});
</script>

View File

@@ -7,7 +7,7 @@
import { computed } from 'vue';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy';
import { defaultStore } from '@/store';
import { customEmojis } from '@/custom-emojis';
import { customEmojisMap } from '@/custom-emojis';
const props = defineProps<{
name: string;
@@ -26,7 +26,7 @@ const rawUrl = computed(() => {
return props.url;
}
if (isLocal.value) {
return customEmojis.value.find(x => x.name === customEmojiName.value)?.url ?? null;
return customEmojisMap.get(customEmojiName.value)?.url ?? null;
}
return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
});

View File

@@ -14,6 +14,7 @@
<script lang="ts" setup>
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
import { $$ } from 'vue/macros';
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const';
const rootEl = $shallowRef<HTMLElement>();
@@ -83,8 +84,8 @@ onMounted(() => {
onUnmounted(() => {
observer.disconnect();
});
defineExpose({
rootEl: $$(rootEl),
});
</script>
<style lang="scss" module>
</style>

View File

@@ -6,7 +6,7 @@
<template v-if="!self">
<span :class="$style.schema">{{ schema }}//</span>
<span :class="$style.hostname">{{ hostname }}</span>
<span v-if="port != ''" :class="$style.port">:{{ port }}</span>
<span v-if="port != ''">:{{ port }}</span>
</template>
<template v-if="pathname === '/' && self">
<span :class="$style.self">{{ hostname }}</span>

View File

@@ -1,6 +1,15 @@
<template>
<section>
<component :is="'h' + h" :class="h < 5 ? $style['h' + h] : null">{{ block.title }}</component>
<component
:is="'h' + h"
:class="{
'h2': h === 2,
'h3': h === 3,
'h4': h === 4,
}"
>
{{ block.title }}
</component>
<div class="_gaps">
<XBlock v-for="child in block.children" :key="child.id" :page="page" :block="child" :h="h + 1"/>

View File

@@ -1,4 +1,4 @@
import { shallowRef, computed, markRaw } from 'vue';
import { shallowRef, computed, markRaw, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { api, apiGet } from './os';
import { useStream } from '@/stream';
@@ -16,6 +16,14 @@ export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
return markRaw([...Array.from(categories), null]);
});
export const customEmojisMap = new Map<string, Misskey.entities.CustomEmoji>();
watch(customEmojis, emojis => {
customEmojisMap.clear();
for (const emoji of emojis) {
customEmojisMap.set(emoji.name, emoji);
}
}, { immediate: true });
// TODO: ここら辺副作用なのでいい感じにする
const stream = useStream();

View File

@@ -13,7 +13,7 @@ type Keys =
'hashtags' |
'wallpaper' |
'theme' |
'colorSchema' |
'colorScheme' |
'useSystemFont' |
'fontSize' |
'ui' |

View File

@@ -149,6 +149,12 @@ const patronsWithIcon = [{
}, {
name: 'かみらえっと',
icon: 'https://misskey-hub.net/patrons/be1326bda7d940a482f3758ffd9ffaf6.jpg',
}, {
name: 'へてて',
icon: 'https://misskey-hub.net/patrons/0431eacd7c6843d09de8ea9984307e86.jpg',
}, {
name: 'spinlock',
icon: 'https://misskey-hub.net/patrons/6a1cebc819d540a78bf20e9e3115baa8.jpg',
}];
const patrons = [

View File

@@ -1,5 +1,5 @@
<template>
<div :class="$style.root" class="_gaps">
<div class="_gaps">
<div :class="$style.header">
<MkSelect v-model="type" :class="$style.typeSelect">
<option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option>
@@ -24,7 +24,7 @@
</button>
</div>
<div v-if="type === 'and' || type === 'or'" :class="$style.values" class="_gaps">
<div v-if="type === 'and' || type === 'or'" class="_gaps">
<Sortable v-model="v.values" tag="div" class="_gaps" itemKey="id" handle=".drag-handle" :group="{ name: 'roleFormula' }" :animation="150" :swapThreshold="0.5">
<template #item="{element}">
<div :class="$style.item">
@@ -118,10 +118,6 @@ function removeSelf() {
</script>
<style lang="scss" module>
.root {
}
.header {
display: flex;
}
@@ -148,8 +144,4 @@ function removeSelf() {
border-color: var(--accent);
}
}
.values {
}
</style>

View File

@@ -28,8 +28,8 @@
<template #icon><i class="ti ti-alert-triangle"></i></template>
<template #label>Errored instances</template>
<template #suffix>({{ number(jobs.reduce((a, b) => a + b[1], 0)) }} jobs)</template>
<div :class="$style.jobs">
<div>
<div v-if="jobs.length > 0">
<div v-for="job in jobs" :key="job[0]">
<MkA :to="`/instance-info/${job[0]}`" behavior="window">{{ job[0] }}</MkA>
@@ -150,7 +150,4 @@ onUnmounted(() => {
font-size: 80%;
opacity: 0.6;
}
.jobs {
}
</style>

View File

@@ -27,7 +27,7 @@
</Sortable>
<div :class="$style.commands">
<MkButton rounded @click="serverRules.push('')"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkButton primary rounded :class="$style.buttonSave" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</div>
</MkSpacer>

View File

@@ -24,6 +24,7 @@
<div class="_gaps_s">
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
<MkSwitch v-model="showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch>
</div>
</FormSection>
@@ -147,7 +148,13 @@
<template #label>{{ i18n.ts.other }}</template>
<div class="_gaps">
<MkSwitch v-model="showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch>
<MkFolder>
<template #label>{{ i18n.ts.additionalEmojiDictionary }}</template>
<div v-for="lang in emojiIndexLangs" class="_buttons">
<MkButton @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ lang }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
<MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</MkFolder>
<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
<FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
</div>
@@ -161,6 +168,8 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkRange from '@/components/MkRange.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
import MkLink from '@/components/MkLink.vue';
@@ -253,6 +262,34 @@ watch([
await reloadAsk();
});
const emojiIndexLangs = ['en-US'];
function downloadEmojiIndex(lang: string) {
async function main() {
const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
function download() {
switch (lang) {
case 'en-US': return import('../../unicode-emoji-indexes/en-US.json').then(x => x.default);
default: throw new Error('unrecognized lang: ' + lang);
}
}
currentIndexes[lang] = await download();
await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
}
os.promiseDialog(main());
}
function removeEmojiIndex(lang: string) {
async function main() {
const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
delete currentIndexes[lang];
await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
}
os.promiseDialog(main());
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);

View File

@@ -32,7 +32,7 @@
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, useCssModule } from 'vue';
import { computed, onMounted, onUnmounted } from 'vue';
import { v4 as uuid } from 'uuid';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue';
@@ -48,8 +48,6 @@ import { definePageMetadata } from '@/scripts/page-metadata';
import { miLocalStorage } from '@/local-storage';
const { t, ts } = i18n;
useCssModule();
const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'menu',
'visibility',

View File

@@ -3,7 +3,7 @@
<div :class="$style.avatarAndBanner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
<div :class="$style.avatarContainer">
<MkAvatar :class="$style.avatar" :user="$i" @click="changeAvatar"/>
<MkButton primary rounded :class="$style.avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
<MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
</div>
<MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
</div>
@@ -271,10 +271,6 @@ definePageMetadata({
margin: 0 auto 16px auto;
}
.avatarEdit {
}
.bannerEdit {
position: absolute;
top: 16px;

View File

@@ -1,5 +1,5 @@
<template>
<div :class="$style.root">
<div>
<MkAnimBg style="position: fixed; top: 0;"/>
<div :class="$style.formContainer">
<form :class="$style.form" class="_panel" @submit.prevent="submit()">
@@ -53,9 +53,6 @@ function submit() {
</script>
<style lang="scss" module>
.root {
}
.formContainer {
min-height: 100svh;
padding: 32px 32px 64px 32px;

View File

@@ -1,5 +1,5 @@
<template>
<div :class="$style.root">
<div>
<MkAnimBg style="position: fixed; top: 0;"/>
<div :class="$style.formContainer">
<form :class="$style.form" class="_panel" @submit.prevent="submit()">
@@ -64,9 +64,6 @@ function submit() {
</script>
<style lang="scss" module>
.root {
}
.formContainer {
min-height: 100svh;
padding: 32px 32px 64px 32px;

View File

@@ -3,7 +3,7 @@
<div ref="scrollEl" :class="[$style.scrollbox, { [$style.scroll]: isScrolling }]">
<div v-for="note in notes" :key="note.id" :class="$style.note">
<div class="_panel" :class="$style.content">
<div :class="$style.body">
<div>
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>

View File

@@ -30,10 +30,10 @@ for (let i = 0; i < emojilist.length; i++) {
export const emojiCharByCategory = _charGroupByCategory;
export function getEmojiName(char: string): string | undefined {
export function getEmojiName(char: string): string | null {
const idx = _indexByChar.get(char);
if (typeof idx === 'undefined') {
return undefined;
if (idx == null) {
return null;
} else {
return emojilist[idx].name;
}

View File

@@ -60,7 +60,7 @@ export function applyTheme(theme: Theme, persist = true) {
document.documentElement.classList.remove('_themeChanging_');
}, 1000);
const colorSchema = theme.base === 'dark' ? 'dark' : 'light';
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
// Deep copy
const _theme = deepClone(theme);
@@ -83,11 +83,11 @@ export function applyTheme(theme: Theme, persist = true) {
document.documentElement.style.setProperty(`--${k}`, v.toString());
}
document.documentElement.style.setProperty('color-schema', colorSchema);
document.documentElement.style.setProperty('color-scheme', colorScheme);
if (persist) {
miLocalStorage.setItem('theme', JSON.stringify(props));
miLocalStorage.setItem('colorSchema', colorSchema);
miLocalStorage.setItem('colorScheme', colorScheme);
}
// 色計算など再度行えるようにクライアント全体に通知

View File

@@ -336,7 +336,11 @@ export const defaultStore = markRaw(new Storage('base', {
},
enableCondensedLineForAcct: {
where: 'device',
default: true,
default: false,
},
additionalUnicodeEmojiIndexes: {
where: 'device',
default: {} as Record<string, Record<string, string[]>>,
},
}));

View File

@@ -22,11 +22,7 @@
}
html {
touch-action: manipulation;
background-color: var(--bg);
background-attachment: fixed;
background-size: cover;
background-position: center;
color: var(--fg);
accent-color: var(--accent);
overflow: auto;
@@ -38,7 +34,7 @@ html {
tab-size: 2;
&, * {
scrollbar-color: var(--scrollbarHandle) inherit;
scrollbar-color: var(--scrollbarHandle) transparent;
scrollbar-width: thin;
&::-webkit-scrollbar {
@@ -87,6 +83,7 @@ html._themeChanging_ {
}
html, body {
touch-action: manipulation;
margin: 0;
padding: 0;
scroll-behavior: smooth;

View File

@@ -21,6 +21,7 @@
fgTransparent: ':alpha<0.5<@fg',
fgHighlighted: ':lighten<3<@fg',
fgOnAccent: '#fff',
fgOnWhite: '#333',
divider: 'rgba(255, 255, 255, 0.1)',
indicator: '@accent',
panel: ':lighten<3<@bg',

View File

@@ -21,6 +21,7 @@
fgTransparent: ':alpha<0.5<@fg',
fgHighlighted: ':darken<3<@fg',
fgOnAccent: '#fff',
fgOnWhite: '#333',
divider: 'rgba(0, 0, 0, 0.1)',
indicator: '@accent',
panel: ':lighten<3<@bg',

View File

@@ -53,6 +53,7 @@
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
htmlThemeColor: '@bg',
fgOnWhite: '@accent',
panelHighlight: ':lighten<3<@panel',
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',

View File

@@ -11,6 +11,7 @@
bg: 'rgb(37, 38, 36)',
fg: 'rgb(216, 212, 199)',
fgHighlighted: '#fff',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.14)',
panel: 'rgb(47, 47, 44)',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@@ -10,6 +10,7 @@
accent: 'rgb(255, 89, 117)',
bg: 'rgb(28, 28, 37)',
fg: 'rgb(236, 239, 244)',
fgOnWhite: '@accent',
panel: 'rgb(35, 35, 47)',
renote: '@accent',
link: '@accent',

View File

@@ -11,6 +11,7 @@
bg: '#232323',
fg: 'rgb(199, 209, 216)',
fgHighlighted: '#fff',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.14)',
panel: '#2d2d2d',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@@ -12,6 +12,7 @@
fg: '#D5D5D6',
fgHighlighted: '#fff',
fgOnAccent: '#000',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.1)',
panel: '#18181c',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@@ -12,6 +12,7 @@
fg: '#dee7e4',
fgHighlighted: '#fff',
fgOnAccent: '#192320',
fgOnWhite: '@accent',
divider: '#e7fffb24',
panel: '#192320',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@@ -12,6 +12,7 @@
fg: '#dee7e4',
fgHighlighted: '#fff',
fgOnAccent: '#192320',
fgOnWhite: '@accent',
divider: '#e7fffb24',
panel: '#192320',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@@ -8,6 +8,7 @@
props: {
accent: '#47BFE8',
fgOnWhite: '@accent',
bg: '#212526',
},
}

View File

@@ -11,6 +11,7 @@
bg: 'rgb(31, 33, 31)',
fg: '#cdd8c7',
fgHighlighted: '#fff',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.14)',
panel: 'rgb(41, 43, 41)',
infoFg: '@fg',

View File

@@ -55,6 +55,7 @@
codeNumber: '#cfff9e',
codeString: '#ffb675',
fgOnAccent: '#fff',
fgOnWhite: '@accent',
infoWarnBg: '#42321c',
infoWarnFg: '#ffbd3e',
navHoverFg: ':lighten<17<@fg',

View File

@@ -10,6 +10,7 @@
accent: 'rgb(234, 154, 82)',
bg: '#e6e5e2',
fg: 'rgb(149, 143, 139)',
fgOnWhite: '@accent',
panel: '#EEECE8',
renote: '@accent',
link: '@accent',

View File

@@ -11,6 +11,7 @@
bg: 'e2deda',
fg: '#3d3d3d',
fgHighlighted: '#6bc9a0',
fgOnWhite: '@accent',
divider: '#cfcfcf',
panel: '@X14',
panelHeaderBg: '@panel',

View File

@@ -10,6 +10,7 @@
accent: 'rgb(219, 96, 114)',
bg: 'rgb(254, 248, 249)',
fg: 'rgb(152, 13, 26)',
fgOnWhite: '@accent',
panel: 'rgb(255, 255, 255)',
renote: '@accent',
link: 'rgb(156, 187, 5)',

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