Compare commits

..

86 Commits

Author SHA1 Message Date
syuilo
23979bf09a 12.48.0 2020-10-17 20:16:17 +09:00
syuilo
7199e6f4e0 Migrate to Vue3 (#6587)
* Update reaction.vue

* fix  bug

* wip

* wip

* wjio

* wip

* Revert "wip"

This reverts commit e427f2160a.

* wip

* wip

* wip

* Update init.ts

* Update drive-window.vue

* wip

* wip

* Use PascalCase for components

* Use PascalCase for components

* update dep

* wip

* wip

* wip

* Update init.ts

* wip

* Update paging.ts

* Update test.vue

* watch deep

* wip

* lint

* wip

* wip

* wip

* wip

* wiop

* wip

* Update webpack.config.ts

* alllow null poll

* wip

* wip

* wip

* wiop

* UI redesign & refactor (#6714)

* wip

* wip

* wip

* wip

* wip

* Update drive.vue

* Update word-mute.vue

* wip

* wip

* wip

* clean up

* wip

* Update default.vue

* wip

* Update notes.vue

* Update mfm.ts

* Update index.home.vue

* Update post-form.vue

* Update post-form-attaches.vue

* wip

* Update post-form.vue

* Update sidebar.vue

* wip

* wip

* Update index.vue

* wip

* Update default.vue

* Update index.vue

* Update index.vue

* wip

* Update post-form-attaches.vue

* Update note.vue

* wip

* clean up

* Update notes.vue

* wip

* wip

* Update ja-JP.yml

* wip

* wip

* Update index.vue

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update default.vue

* wip

* Update _dark.json5

* wip

* wip

* wip

* clean up

* wip

* wip

* Update index.vue

* Update test.vue

* wip

* wip

* fix

* wip

* wip

* wip

* wip

* clena yop

* wip

* wip

* Update store.ts

* Update messaging-room.vue

* Update default.widgets.vue

* fix

* wip

* wip

* Update modal.vue

* wip

* Update os.ts

* Update os.ts

* Update deck.vue

* Update init.ts

* wip

* Update ja-JP.yml

* v-sizeは単にwindowのresizeを監視するだけで良いかもしれない

* Update modal.vue

* wip

* Update tooltip.ts

* wip

* wip

* wip

* wip

* wip

* Update image-viewer.vue

* wip

* wip

* Update style.scss

* Update style.scss

* Update visitor.vue

* wip

* Update init.ts

* Update init.ts

* wip

* wip

* Update visitor.vue

* Update visitor.vue

* Update visitor.vue

* Update visitor.vue

* wip

* wip

* Update modal.vue

* Update header.vue

* Update menu.vue

* Update about.vue

* Update about-misskey.vue

* wip

* wip

* Update visitor.vue

* Update tooltip.ts

* wip

* Update drive.vue

* wip

* Update style.scss

* Update header.vue

* wip

* wip

* Update users.user.vue

* Update announcements.vue

* wip

* wip

* wip

* Update emojis.vue

* wip

* Update emojis.vue

* Update style.scss

* Update users.vue

* wip

* Update style.scss

* wip

* Update welcome.entrance.vue

* Update radio.vue

* Update size.ts

* Update emoji-edit-dialog.vue

* wip

* Update emojis.vue

* wip

* Update emojis.vue

* Update emojis.vue

* Update emojis.vue

* wip

* wip

* wip

* wip

* Update file-dialog.vue

* wip

* wip

* Update token-generate-window.vue

* Update notification-setting-window.vue

* wip

* wip

* Update _error_.vue

* Update ja-JP.yml

* wip

* wip

* Update store.ts

* Update emojis.vue

* Update emojis.vue

* Update emojis.vue

* Update announcements.vue

* Update store.ts

* wip

* Update page-editor.vue

* wip

* wip

* Update modal.vue

* wip

* Update select-file.ts

* Update timeline.vue

* Update emojis.vue

* Update os.ts

* wip

* Update user-select.vue

* Update mfm.ts

* Update get-file-info.ts

* Update drive.vue

* Update init.ts

* Update mfm.ts

* wip

* wip

* Update window.vue

* Update note.vue

* wip

* wip

* Update user-info.vue

* wip

* wip

* wip

* wip

* wip

* Update header.vue

* Update header.vue

* wip

* Update explore.vue

* wip

* wip

* wip

* Update webpack.config.ts

* wip

* wip

* wip

* wip

* wip

* wip

* Update autocomplete.ts

* wip

* wip

* wip

* Update toast.vue

* wip

* Update post-form-dialog.vue

* wip

* wip

* wip

* wip

* wip

* Update users.vue

* wip

* Update explore.vue

* wip

* wip

* wip

* Update package.json

* wip

* Update icon-dialog.vue

* wip

* wip

* Update user-preview.ts

* wip

* wip

* wip

* wip

* wip

* Update instance.vue

* Update user-name.vue

* Update federation.vue

* Update instance.vue

* wip

* wip

* Update tag.vue

* wip

* wip

* wip

* wip

* wip

* Update instance.vue

* wip

* Update os.ts

* Update os.ts

* wip

* wip

* wip

* Update router.ts

* wip

* Update init.ts

* Update note.vue

* Update messages.vue

* wip

* wip

* wip

* wip

* wip

* google

* wip

* wip

* wip

* wip

* Update theme-editor.vue

* wip

* wip

* Update room.vue

* Update channel-editor.vue

* wip

* Update window.vue

* Update window.vue

* wip

* Update window.vue

* Update window.vue

* wip

* Update menu.vue

* wip

* wip

* wip

* wip

* Update messaging-room.vue

* wip

* Update post-form.vue

* Update default.widgets.vue

* Update window.vue

* wip
2020-10-17 20:12:00 +09:00
sobadon
a40f38b2b5 CW の input でも投稿ショートカットが動作するように (#6690) 2020-10-09 14:22:32 +09:00
MeiMei
00a17ed5d4 /streamingに非WebSocketリクエストが来るとおかしくなるのを修正 Fix #6718 (#6719) 2020-10-09 14:20:34 +09:00
MeiMei
b594366f06 Update resolutions (#6723) 2020-10-09 14:17:15 +09:00
MeiMei
e9284930df Update nested-property (#6720) 2020-10-01 19:51:34 +09:00
MeiMei
ea7504f564 匿名ユーザーでapp/showをリクエストすると500を返すのを修正 Fix #6715 (#6716) 2020-09-28 21:27:05 +09:00
sobadon
c1f6d996f6 Fix: Channel 投稿を削除編集すると Channel 外に投稿されるのを修正 (#6707) 2020-09-22 00:54:24 +09:00
syuilo
df71dbb024 Resolve #6692 (#6703) 2020-09-18 22:18:21 +09:00
syuilo
f104e9b6cc chore: better error text 2020-09-17 21:05:47 +09:00
syuilo
e29b5c2326 fix(client): Fix #6698 2020-09-11 21:50:44 +09:00
MeiMei
925868dcdb Update dependencies (#6678) 2020-08-30 18:20:14 +09:00
takonomura
d7df26d92b Fix channels list pagination (#6679) 2020-08-30 18:18:34 +09:00
syuilo
42d1c67d56 fix(server): Fix #6669 2020-08-29 09:39:50 +09:00
MeiMei
c2d7929391 Expose proxyAccountName (#6670) 2020-08-29 08:56:32 +09:00
tamaina
5864b52a81 Update create-notification.ts 2020-08-29 03:26:44 +09:00
tamaina
493d32b3dc Fix #6676 2020-08-29 03:14:27 +09:00
Xeltica
ed141338fb Safari で mk-select に光沢がかかるのを修正 (#6668) 2020-08-23 11:05:04 +09:00
syuilo
95db488c48 12.47.1 2020-08-22 10:11:46 +09:00
syuilo
d5e1e523b6 New Crowdin updates (#6661)
* New translations ja-JP.yml (Chinese Simplified)

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

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

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

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)
2020-08-22 10:11:14 +09:00
Xeltica
cd0f8a4ef9 表示する通知を種別ごとに設定できるように (#6647)
* ストリーミング以外は一通り実装

* ストリーミング分も適用

* 通知のグローバル設定をサーバーサイドに保存

* グローバル通知を使うようにしたら更新されなくなるのを修正

* サーバーサイド処理

* i/notifications のパラメーター includeTypes に空配列を渡すと全部の通知が来る問題を修正

* 全て有効/無効ボタンを実装

* Squashed commit of the following:

commit c3c111529e
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Wed Aug 19 22:29:04 2020 +0900

    12.47.0

commit 2dbab66cfe
Author: syuilo <Syuilotan@yahoo.co.jp>
Date:   Wed Aug 19 22:24:39 2020 +0900

    New Crowdin updates (#6617)

    * New translations ja-JP.yml (French)

    * New translations ja-JP.yml (Arabic)

    * New translations ja-JP.yml (French)

    * New translations ja-JP.yml (Spanish)

    * New translations ja-JP.yml (German)

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

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (Spanish)

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

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

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

    * New translations ja-JP.yml (Spanish)

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

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (Spanish)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

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

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

    * New translations ja-JP.yml (Korean)

    * New translations ja-JP.yml (Korean)

    * New translations ja-JP.yml (Korean)

    * New translations ja-JP.yml (Spanish)

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

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

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

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

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

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

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

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

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

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (Korean)

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

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (Spanish)

    * New translations ja-JP.yml (Arabic)

    * New translations ja-JP.yml (French)

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

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

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

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

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

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

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (Korean)

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

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (Spanish)

    * New translations ja-JP.yml (Arabic)

    * New translations ja-JP.yml (French)

    * 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 Simplified)

commit 01238d6b1a
Author: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Date:   Wed Aug 19 22:24:02 2020 +0900

    Update README.md [AUTOGEN] (#6593)

commit c34f302b1c
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Wed Aug 19 21:47:18 2020 +0900

    enhance(client): サーバーから切断されたときにダイアログで警告を表示できるように

commit 6870262f8d
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Wed Aug 19 17:52:11 2020 +0900

    enhance(client): Better element visible detection

commit c54d5e7040
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Wed Aug 19 17:51:31 2020 +0900

    fix(clinet): 誤字によりスクロールイベントリスナが解除されていなかったのを修正

commit 0ace009a54
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Tue Aug 18 22:52:54 2020 +0900

    fix(server): Prevent error when recieve non-json data from websocket

    Fix #6658

commit 48e8ee440b
Author: MeiMei <30769358+mei23@users.noreply.github.com>
Date:   Tue Aug 18 22:48:52 2020 +0900

    WebPのアニメーションが失われるのを修正 Fix #6625 (#6649)

commit 9855405b89
Author: syuilo <Syuilotan@yahoo.co.jp>
Date:   Tue Aug 18 22:44:21 2020 +0900

    Channel (#6621)

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wop

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * add notes

    * wip

    * wip

    * wip

    * wip

    * sound

    * wip

    * add kick_gaba2

    * wip

commit 122076e8ea
Author: MeiMei <30769358+mei23@users.noreply.github.com>
Date:   Sat Aug 15 04:27:19 2020 +0900

    Sign (request-target) Fix #6652 (#6656)

commit 7c5ac2cbb4
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Fri Aug 14 15:24:55 2020 +0900

    perf(server): Add isSensitive index to improve query performance

commit ccda2181c1
Author: MeiMei <30769358+mei23@users.noreply.github.com>
Date:   Fri Aug 14 00:54:33 2020 +0900

    GCSに大きいファイルがアップロードできないのを修正 Fix #6254 (#6648)

commit b5fe4ba9be
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Thu Aug 13 23:02:43 2020 +0900

    WIP: Improve admin dashboard

commit fd9c7d525a
Merge: 080574e13 ee0a44559
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Thu Aug 13 21:27:10 2020 +0900

    Merge branch 'develop' of https://github.com/syuilo/misskey into develop

commit 080574e13d
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Thu Aug 13 21:27:06 2020 +0900

    WIP: Improve admin dashboard

commit ee0a445590
Author: MeiMei <30769358+mei23@users.noreply.github.com>
Date:   Thu Aug 13 20:05:01 2020 +0900

    Option objectStorageSetPublicRead (#6645)

commit bb342c7601
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Thu Aug 13 19:56:46 2020 +0900

    WIP: Improve admin dashboard

commit ed17636fb9
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Thu Aug 13 17:58:16 2020 +0900

    WIP: Improve admin dashboard

commit c59d7d941a
Author: syuilo <Syuilotan@yahoo.co.jp>
Date:   Wed Aug 12 17:42:12 2020 +0900

    Update README.md

    Close #6644

commit 377377595a
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 20:23:51 2020 +0900

    enhance(client): Improve admin page

commit d63aef9963
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 13:55:00 2020 +0900

    chore(client): Fix style

commit e9b28fa3c0
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 13:00:10 2020 +0900

    chore(client): Design tweaks

commit be255dc583
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 12:42:51 2020 +0900

    chore(client): Design tweak

commit 18eb7c6087
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 12:31:22 2020 +0900

    chore(client): Design tweaks

commit cf29e69813
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 12:28:35 2020 +0900

    chore(client): Fix bug

commit 132da7e3c0
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 12:20:58 2020 +0900

    Update ja-JP.yml

commit 26df23bb64
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 12:18:02 2020 +0900

    chore(client): fix style

commit 76389ad619
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 12:15:58 2020 +0900

    chore(client): Design tweaks

commit 7cde8cfbf2
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 11:51:43 2020 +0900

    chore(client): Design tweaks

commit 4eb2ddac4e
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 11:24:30 2020 +0900

    chore(client): Design tweaks

commit dc51eef27c
Merge: bff8a23cb 9c5efb9da
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 10:38:00 2020 +0900

    Merge branch 'develop' of https://github.com/syuilo/misskey into develop

commit bff8a23cbc
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 10:37:57 2020 +0900

    chore(client): Design tweaks

commit 9c5efb9da0
Author: rinsuki <428rinsuki+git@gmail.com>
Date:   Mon Aug 10 01:33:01 2020 +0900

    Dockerのビルド時にgitを入れるように (#6639)

    917d3d0bd3 でgitの依存関係が追加されたのにgitが入っていないのでコケていた

commit 48b8320e5e
Author: rinsuki <428rinsuki+git@gmail.com>
Date:   Mon Aug 10 01:32:27 2020 +0900

    Fix #6637 (#6638)

    * Fix #6637

    * fix lint

commit 9b2ed96c1c
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sun Aug 9 15:59:38 2020 +0900

    chore: Clean up

commit 69d9aa71f2
Author: syuilo <Syuilotan@yahoo.co.jp>
Date:   Sun Aug 9 15:51:02 2020 +0900

    Full view mode (#6636)

    * wuip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * Update folder.vue

    * wip

    * Update size.ts

    * wip

    * wip

    * Update index.vue

    * wip

commit 13683780cd
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sun Aug 9 13:49:44 2020 +0900

    ✌️

commit d780e5b251
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sun Aug 9 13:46:19 2020 +0900

    enhance(client): ミュートされたノート数を表示するようにしたり

commit 917d3d0bd3
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sat Aug 8 10:30:38 2020 +0900

    chore: Update dependencies 🚀

commit 4b19c53697
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sat Aug 8 10:27:37 2020 +0900

    client: テーマコードをコピーできるようにしたり

commit 2d40a15d2b
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Fri Aug 7 11:27:37 2020 +0900

    refactor: Extract well-known services

commit 2bdcd22ad4
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Tue Aug 4 23:09:48 2020 +0900

    enhance(api): アクセストークンを作成する際、createdAtをlastUsedAtを揃えるようにして、未使用かどうかを判定できるように

commit f73a4e1304
Author: MeiMei <30769358+mei23@users.noreply.github.com>
Date:   Tue Aug 4 21:12:55 2020 +0900

    Update .dockerignore (#6620)

commit b265cdbd84
Author: Xeltica <7106976+Xeltica@users.noreply.github.com>
Date:   Mon Aug 3 13:40:32 2020 +0900

    Update CHANGELOG.md

commit a04d8b95c2
Author: Xeltica <7106976+Xeltica@users.noreply.github.com>
Date:   Mon Aug 3 13:40:13 2020 +0900

    Update CHANGELOG.md

commit 0e9a8c0cd4
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sun Aug 2 13:59:05 2020 +0900

    fix(client): Message read state is not reactive

commit 5ae8a3c7e8
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sun Aug 2 13:49:28 2020 +0900

    refactor

* fix: includeTypes 未指定時に通知が返ってこなくなるバグを修正

* 最適化とバグ修正

* 挙動を修正

* Update ja-JP.yml

* 不要なimportを削除

* ✌

* 不要なコードの削除

* Update notification-setting-window.vue

* Update notification-setting-window.vue

* 🎨

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2020-08-22 10:06:17 +09:00
syuilo
6dac505af9 Update dependencies 🚀 2020-08-22 08:03:11 +09:00
coord_e
9d398040cb fix an error on /api-doc (#6665) 2020-08-22 04:25:47 +09:00
coord_e
aa55acedc9 Fix not to reject non-image file uploads (#6664)
* fix not to reject non-image file uploads

* handle an error from sharp
2020-08-22 04:25:25 +09:00
Takeshi Umeda
eb70d6f226 Fix a slow query on channel timeline (#6663) 2020-08-20 17:17:55 +09:00
Xeltica
36f0963d78 Update CHANGELOG.md 2020-08-19 23:09:21 +09:00
syuilo
c3c111529e 12.47.0 2020-08-19 22:29:04 +09:00
syuilo
2dbab66cfe New Crowdin updates (#6617)
* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

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

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

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

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

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

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

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

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

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

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

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

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (French)

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

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

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

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

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (French)

* 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 Simplified)
2020-08-19 22:24:39 +09:00
Acid Chicken (硫酸鶏)
01238d6b1a Update README.md [AUTOGEN] (#6593) 2020-08-19 22:24:02 +09:00
syuilo
c34f302b1c enhance(client): サーバーから切断されたときにダイアログで警告を表示できるように 2020-08-19 21:47:18 +09:00
syuilo
6870262f8d enhance(client): Better element visible detection 2020-08-19 17:52:11 +09:00
syuilo
c54d5e7040 fix(clinet): 誤字によりスクロールイベントリスナが解除されていなかったのを修正 2020-08-19 17:51:31 +09:00
syuilo
0ace009a54 fix(server): Prevent error when recieve non-json data from websocket
Fix #6658
2020-08-18 22:52:54 +09:00
MeiMei
48e8ee440b WebPのアニメーションが失われるのを修正 Fix #6625 (#6649) 2020-08-18 22:48:52 +09:00
syuilo
9855405b89 Channel (#6621)
* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wop

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* add notes

* wip

* wip

* wip

* wip

* sound

* wip

* add kick_gaba2

* wip
2020-08-18 22:44:21 +09:00
MeiMei
122076e8ea Sign (request-target) Fix #6652 (#6656) 2020-08-15 04:27:19 +09:00
syuilo
7c5ac2cbb4 perf(server): Add isSensitive index to improve query performance 2020-08-14 15:24:55 +09:00
MeiMei
ccda2181c1 GCSに大きいファイルがアップロードできないのを修正 Fix #6254 (#6648) 2020-08-14 00:54:33 +09:00
syuilo
b5fe4ba9be WIP: Improve admin dashboard 2020-08-13 23:02:43 +09:00
syuilo
fd9c7d525a Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-08-13 21:27:10 +09:00
syuilo
080574e13d WIP: Improve admin dashboard 2020-08-13 21:27:06 +09:00
MeiMei
ee0a445590 Option objectStorageSetPublicRead (#6645) 2020-08-13 20:05:01 +09:00
syuilo
bb342c7601 WIP: Improve admin dashboard 2020-08-13 19:56:46 +09:00
syuilo
ed17636fb9 WIP: Improve admin dashboard 2020-08-13 17:58:16 +09:00
syuilo
c59d7d941a Update README.md
Close #6644
2020-08-12 17:42:12 +09:00
syuilo
377377595a enhance(client): Improve admin page 2020-08-10 20:23:51 +09:00
syuilo
d63aef9963 chore(client): Fix style 2020-08-10 13:55:00 +09:00
syuilo
e9b28fa3c0 chore(client): Design tweaks 2020-08-10 13:00:10 +09:00
syuilo
be255dc583 chore(client): Design tweak 2020-08-10 12:42:51 +09:00
syuilo
18eb7c6087 chore(client): Design tweaks 2020-08-10 12:31:22 +09:00
syuilo
cf29e69813 chore(client): Fix bug 2020-08-10 12:28:35 +09:00
syuilo
132da7e3c0 Update ja-JP.yml 2020-08-10 12:20:58 +09:00
syuilo
26df23bb64 chore(client): fix style 2020-08-10 12:18:02 +09:00
syuilo
76389ad619 chore(client): Design tweaks 2020-08-10 12:15:58 +09:00
syuilo
7cde8cfbf2 chore(client): Design tweaks 2020-08-10 11:51:43 +09:00
syuilo
4eb2ddac4e chore(client): Design tweaks 2020-08-10 11:24:30 +09:00
syuilo
dc51eef27c Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-08-10 10:38:00 +09:00
syuilo
bff8a23cbc chore(client): Design tweaks 2020-08-10 10:37:57 +09:00
rinsuki
9c5efb9da0 Dockerのビルド時にgitを入れるように (#6639)
917d3d0bd3 でgitの依存関係が追加されたのにgitが入っていないのでコケていた
2020-08-10 01:33:01 +09:00
rinsuki
48b8320e5e Fix #6637 (#6638)
* Fix #6637

* fix lint
2020-08-10 01:32:27 +09:00
syuilo
9b2ed96c1c chore: Clean up 2020-08-09 15:59:38 +09:00
syuilo
69d9aa71f2 Full view mode (#6636)
* wuip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update folder.vue

* wip

* Update size.ts

* wip

* wip

* Update index.vue

* wip
2020-08-09 15:51:02 +09:00
syuilo
13683780cd ✌️ 2020-08-09 13:49:44 +09:00
syuilo
d780e5b251 enhance(client): ミュートされたノート数を表示するようにしたり 2020-08-09 13:46:19 +09:00
syuilo
917d3d0bd3 chore: Update dependencies 🚀 2020-08-08 10:30:38 +09:00
syuilo
4b19c53697 client: テーマコードをコピーできるようにしたり 2020-08-08 10:27:37 +09:00
syuilo
2d40a15d2b refactor: Extract well-known services 2020-08-07 11:27:37 +09:00
syuilo
2bdcd22ad4 enhance(api): アクセストークンを作成する際、createdAtをlastUsedAtを揃えるようにして、未使用かどうかを判定できるように 2020-08-04 23:09:48 +09:00
MeiMei
f73a4e1304 Update .dockerignore (#6620) 2020-08-04 21:12:55 +09:00
Xeltica
b265cdbd84 Update CHANGELOG.md 2020-08-03 13:40:32 +09:00
Xeltica
a04d8b95c2 Update CHANGELOG.md 2020-08-03 13:40:13 +09:00
syuilo
0e9a8c0cd4 fix(client): Message read state is not reactive 2020-08-02 13:59:05 +09:00
syuilo
5ae8a3c7e8 refactor 2020-08-02 13:49:28 +09:00
syuilo
70ee172128 12.46.0 2020-08-02 00:10:30 +09:00
syuilo
b9febc00f9 Update aiscript 2020-08-02 00:09:54 +09:00
syuilo
0112e2f7ec New Crowdin updates (#6611)
* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Spanish)

* 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 (Chinese Simplified)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (Korean)
2020-08-01 23:44:07 +09:00
syuilo
60736bab2a fix(client): Broken syntax highlight 2020-08-01 23:30:51 +09:00
rinsuki
fb7c4ee21a チャットでCmd+Enterできないのを修正 (#6614) 2020-08-01 21:50:21 +09:00
syuilo
e93c06cd00 fix appearance 2020-08-01 18:01:48 +09:00
syuilo
0a99345909 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-08-01 16:39:52 +09:00
syuilo
3d08ff7cb4 🎨 2020-08-01 16:39:48 +09:00
syuilo
057ce73ba1 refactor 2020-08-01 12:04:30 +09:00
syuilo
66b07578c5 Fold sidebar (#6610)
* wip

* wip
2020-08-01 10:53:23 +09:00
syuilo
de3b365563 chore: Remove debug code 2020-08-01 10:03:47 +09:00
syuilo
7bb8d8b27e refactor(client): Fix order of component property 2020-08-01 10:02:37 +09:00
syuilo
9008664606 fix(client): Cannot read announcement
Fix #6609
2020-08-01 10:02:03 +09:00
452 changed files with 18874 additions and 12674 deletions

View File

@@ -5,6 +5,7 @@
.vscode
Dockerfile
build/
built/
db/
docker-compose.yml
elasticsearch/

View File

@@ -1,5 +1,62 @@
ChangeLog
=========
12.47.0 (2020/8/19)
------------------
### ✨Improvements
- **チャンネル機能を実装**
- 管理パネルを刷新
- ワードミュートの設定欄から、ミュートされたノート数を閲覧できるように
- テーマコードをコピーできるように
- オブジェクトストレージ設定に `アップロード時に'public-read'を設定する` を追加
- サーバーから切断されたときの警告をダイアログで表示するオプションを追加
### 🐛Fixes
- ユーザーのトークンが管理画面で見られるようになっていた不具合を修正
- GCSに大きいファイルがアップロードできない不具合を修正
- WebP 画像ファイルのアニメーションが失われる不具合を修正
12.46.0 (2020/8/2)
------------------
### ✨Improvements
- チャットでCmd+Enterショートカットが使用できない問題を修正 [#6614](https://github.com/syuilo/misskey/pull/6614)
- サイドバーを折り畳めるように [#6610](https://github.com/syuilo/misskey/pull/6610)
### 🐛Fixes
- お知らせを既読にできない問題を修正 [9008664](https://github.com/syuilo/misskey/commit/9008664606483e2902f03929f0f696ac43de6db4)
- シンタックスハイライト構文が壊れている問題を修正 [60736ba](https://github.com/syuilo/misskey/commit/60736bab2ac974c2d2c2c106d297fa67fdaff87a)
12.45.1 (2020/8/1)
-------------------
### ✨Improvements
- 自分のノートにリアクションを押せるように [#6506](https://github.com/syuilo/misskey/pull/6506)
- 非ログイン時にウェルカムメッセージが被る問題を修正 [#6509](https://github.com/syuilo/misskey/pull/6509)
### 🐛Fixes
- 最新の投票結果がタイムラインなどに反映されない問題を修正 [2522e73](https://github.com/syuilo/misskey/commit/2522e7388d4d0b92d4517f2c07190e8f88394026)
12.45.0 (2020/7/30)
-------------------
### ✨Improvements
- プラグインのIDを不要に [57203de](https://github.com/syuilo/misskey/commit/57203de4cbf3947825f422dd746a076d79e353c7)
- プラグインの設定にdescriptionを表示できるように [9eee564](https://github.com/syuilo/misskey/commit/9eee5644b9b112ed6d8863edce569f4d554459f5)
- AiScript: Plugin:register_note_post_interruptor 関数を追加(ノート作成時の割り込み処理を登録できる) [e7de5f6](https://github.com/syuilo/misskey/commit/e7de5f60513774e9c599a2e3aac0fbeefb88236f)
- AiScript: Plugin:open_url 関数を追加 [60d81d7](https://github.com/syuilo/misskey/commit/60d81d74e35879f52a374d5e35fe25dc115d75a4)
### 🐛Fixes
- 通知のノートがリアクティブではない問題を修正 [2701a7e](https://github.com/syuilo/misskey/commit/2701a7e85fcf745e75b46b88b0fc9b3f76218e44)
- ピン留めされたノートがリアクティブではない問題を修正 [31a0afd](https://github.com/syuilo/misskey/commit/31a0afdaab309cd2e9fd22f0524730488202704d)
- プラグインの設定がnullになることがある問題を修正 [01e9b3c](https://github.com/syuilo/misskey/commit/01e9b3c2f634f37cee6820ca25d7576ef3ab6442)
12.44.1 (2020/7/29)
-------------------
### 🐛Fixes

View File

@@ -12,6 +12,7 @@ RUN apk add --no-cache \
autoconf \
automake \
file \
git \
g++ \
gcc \
libc-dev \

View File

@@ -6,6 +6,7 @@
[![CircleCI](https://img.shields.io/circleci/project/github/syuilo/misskey.svg?style=for-the-badge&logo=circleci)](https://circleci.com/gh/syuilo/misskey)
[![Dependencies](https://img.shields.io/david/syuilo/misskey.svg?style=for-the-badge&logo=npm)](https://david-dm.org/syuilo/misskey)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge&logo=github)](http://makeapullrequest.com)
[![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
**A forever evolving, sophisticated microblogging platform.**
@@ -130,7 +131,6 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c8.patreon.com/2/200/557245" alt="mkatze " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/23915207/25428766ecd745478e600b3d7f871eb2/1.png?token-time=2145916800&token-hash=urCLLA4KjJZX92Y1CxcBP4d8bVTHGkiaPnQZp-Tqz68%3D" alt="kabo2468y " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/8249688/4aacf36b6b244ab1bc6653591b6640df/2.png?token-time=2145916800&token-hash=1ZEf2w6L34253cZXS_HlVevLEENWS9QqrnxGUAYblPo%3D" alt="AureoleArk " width="100"></td>
<td><img src="https://c8.patreon.com/2/200/21285325" alt="Nie(sha) " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5670915/ee175f0bfb6347ffa4ea101a8c097bff/1.jpg?token-time=2145916800&token-hash=mPLM9CA-riFHx-myr3bLZJuH2xBRHA9se5VbHhLIOuA%3D" alt="osapon " width="100"></td>
<td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/36813045/29876ea679d443bcbba3c3f16edab8c2/2.jpeg?token-time=2145916800&token-hash=YCKWnIhrV9rjUCV9KqtJnEqjy_uGYF3WMXftjUdpi7o%3D" alt="Wataru Manji (manji0)" width="100"></td>
@@ -141,7 +141,6 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=557245">mkatze </a></td>
<td><a href="https://www.patreon.com/user?u=23915207">kabo2468y </a></td>
<td><a href="https://www.patreon.com/AureoleArk">AureoleArk </a></td>
<td><a href="https://www.patreon.com/user?u=21285325">Nie(sha) </a></td>
<td><a href="https://www.patreon.com/osapon">osapon </a></td>
<td><a href="https://www.patreon.com/user?u=16869916">見当かなみ </a></td>
<td><a href="https://www.patreon.com/user?u=36813045">Wataru Manji (manji0)</a></td>
@@ -153,8 +152,8 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c8.patreon.com/2/200/16542964" alt="Takumi Sugita" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/17866454" alt="sikyosyounin " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3.png?token-time=2145916800&token-hash=KjfQL8nf3AIf6WqzLshBYAyX44piAqOAZiYXgZS_H6A%3D" alt="YUKIMOCHI" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/38837364/9421361c54c645ac8f5fc442a40c32e9/1.png?token-time=2145916800&token-hash=TUZB48Nem3BeUPLBH6s3P6WyKBnQOy0xKaDSTBBUNzA%3D" alt="xianon" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/26340354/08834cf767b3449e93098ef73a434e2f/2.png?token-time=2145916800&token-hash=nyM8DnKRL8hR47HQ619mUzsqVRpkWZjgtgBU9RY15Uc%3D" alt="totokoro " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19356899/496b4681d33b4520bd7688e0fd19c04d/2.jpeg?token-time=2145916800&token-hash=_sTj3dUBOhn9qwiJ7F19Qd-yWWfUqJC_0jG1h0agEqQ%3D" alt="sheeta.s " width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61 </a></td>
<td><a href="https://www.patreon.com/hs_sh_net">mewl hayabusa</a></td>
@@ -162,10 +161,11 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=16542964">Takumi Sugita</a></td>
<td><a href="https://www.patreon.com/user?u=17866454">sikyosyounin </a></td>
<td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td>
<td><a href="https://www.patreon.com/user?u=38837364">xianon</a></td>
<td><a href="https://www.patreon.com/user?u=26340354">totokoro </a></td>
<td><a href="https://www.patreon.com/user?u=19356899">sheeta.s </a></td>
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19356899/496b4681d33b4520bd7688e0fd19c04d/2.jpeg?token-time=2145916800&token-hash=_sTj3dUBOhn9qwiJ7F19Qd-yWWfUqJC_0jG1h0agEqQ%3D" alt="sheeta.s " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5827393/59893c191dda408f9cabd0f20a3a5627/1.jpeg?token-time=2145916800&token-hash=i9N05vOph-eP1LTLb9_npATjYOpntL0ZsHNaZFSsPmE%3D" alt="motcha " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/20494440/540beaf2445f408ea6597bc61e077bb3/1.png?token-time=2145916800&token-hash=UJ0JQge64Bx9XmN_qYA1inMQhrWf4U91fqz7VAKJeSg%3D" alt="axtuki1 " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13737140/1adf7835017d479280d90fe8d30aade2/1.png?token-time=2145916800&token-hash=0pdle8h5pDZrww0BDOjdz6zO-HudeGTh36a3qi1biVU%3D" alt="Satsuki Yanagi" width="100"></td>
@@ -176,8 +176,8 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/26144593/9514b10a5c1b42a3af58621aee213d1d/1.png?token-time=2145916800&token-hash=v1PYRsjzu4c_mndN4Hvi_dlispZJsuGRCQeNS82pUSM%3D" alt="EBISUME" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG " width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/user?u=19356899">sheeta.s </a></td>
<td><a href="https://www.patreon.com/user?u=5827393">motcha </a></td>
<td><a href="https://www.patreon.com/user?u=20494440">axtuki1 </a></td>
<td><a href="https://www.patreon.com/user?u=13737140">Satsuki Yanagi</a></td>
@@ -188,9 +188,9 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
<td><a href="https://www.patreon.com/user?u=26144593">EBISUME</a></td>
<td><a href="https://www.patreon.com/noellabo">noellabo </a></td>
<td><a href="https://www.patreon.com/Corset">CG </a></td>
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpg?token-time=2145916800&token-hash=7bkMqTwHPRsJPGAq42PYdDXDZBVGLqdgr1ZmBxX8GFQ%3D" alt="Hekovic " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24641572/b4fd175424814f15b0ca9178d2d2d2e4/1.png?token-time=2145916800&token-hash=e2fyqdbuJbpCckHcwux7rbuW6OPkKdERcus0u2wIEWU%3D" alt="uroco @99" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/14661394" alt="Chandler " width="100"></td>
@@ -199,6 +199,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9481273/7fa89168e72943859c3d3c96e424ed31/4.jpeg?token-time=2145916800&token-hash=5w1QV1qXe-NdWbdFmp1H7O_-QBsSiV0haumk3XTHIEg%3D" alt="Efertone " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1.jpeg?token-time=2145916800&token-hash=vGe7wXGqmA8Q7m-kDNb6fyGdwk-Dxk4F-ut8ZZu51RM%3D" alt="Takashi Shibuya" width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/Corset">CG </a></td>
<td><a href="https://www.patreon.com/hekovic">Hekovic </a></td>
<td><a href="https://www.patreon.com/user?u=24641572">uroco @99</a></td>
<td><a href="https://www.patreon.com/user?u=14661394">Chandler </a></td>
@@ -208,7 +209,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table>
**Last updated:** Tue, 14 Jul 2020 09:00:09 UTC
**Last updated:** Sun, 26 Jul 2020 07:00:10 UTC
<!-- PATREON_END -->
[backer-url]: #backers

View File

@@ -7,9 +7,6 @@ import * as gulp from 'gulp';
import * as ts from 'gulp-typescript';
import * as rimraf from 'rimraf';
import * as rename from 'gulp-rename';
const cleanCSS = require('gulp-clean-css');
const sass = require('gulp-dart-sass');
const fiber = require('fibers');
const locales: { [x: string]: any } = require('./locales');
const meta = require('./package.json');
@@ -61,13 +58,6 @@ gulp.task('cleanall', gulp.parallel('clean', cb =>
rimraf('./node_modules', cb)
));
gulp.task('build:client:styles', () =>
gulp.src('./src/client/style.scss')
.pipe(sass({ fiber }))
.pipe(cleanCSS())
.pipe(gulp.dest('./built/client/assets/'))
);
gulp.task('copy:client', () =>
gulp.src([
'./assets/**/*',
@@ -87,7 +77,6 @@ gulp.task('copy:docs', () =>
);
gulp.task('build:client', gulp.parallel(
'build:client:styles',
'copy:client',
'copy:docs'
));

View File

@@ -256,7 +256,6 @@ unregister: "إلغاء التسجيل"
passwordLessLogin: "لِج مِن دون كلمة سرية"
resetPassword: "أعد تعيين كلمتك السرية"
newPasswordIs: "كلمتك السرية الجديدة هي {password}"
autoReloadWhenDisconnected: "إنعاش تلقائي عندما يُقطَع الإتصال بالخادم"
autoNoteWatch: "راقب الملاحظات تلقائيا"
share: "شارِك"
notFound: "غير موجود"
@@ -351,6 +350,13 @@ pluginInstallWarn: "يرجى تنصيب إضافات ذات مصدر موثوق
smtpHost: "المضيف"
smtpUser: "اسم المستخدم"
smtpPass: "الكلمة السرية"
display: "المظهر"
_channel:
featured: "المتداوَلة"
_sidebar:
full: "كامل"
icon: "الصورة الرمزية"
hide: "إخفاء"
_theme:
explore: "استكشف قوالب المظهر"
install: "تنصيب قالب"

View File

@@ -84,14 +84,14 @@ quote: "Zitieren"
pinnedNote: "Angepinnte Notiz"
you: "Du"
clickToShow: "Klicke, um diesen Inhalt anzusehen"
sensitive: "Dieser Inhalt ist NSFW"
sensitive: "NSFW"
add: "Hinzufügen"
reaction: "Reaktionen"
reactionSettingDescription: "Gib deine Lieblingsreaktionen ein, um sie der Reaktionsauswahl hinzuzufügen."
rememberNoteVisibility: "Notizsichtbarkeit merken"
attachCancel: "Anhang entfernen"
markAsSensitive: "Als sensitiv markieren"
unmarkAsSensitive: "Markierung als sensitiv zurücknehmen"
markAsSensitive: "Als NSFW markieren"
unmarkAsSensitive: "Markierung als NSFW zurücknehmen"
enterFileName: "Dateinamen eingeben"
mute: "Stummschalten"
unmute: "Stummschaltung aufheben"
@@ -165,7 +165,7 @@ clearCachedFilesConfirm: "Sollen alle im Cache gespeicherten Dateien von anderen
blockedInstances: "Blockierte Instanzen"
blockedInstancesDescription: "Gib den Hostnamen der Instanz an, die blockiert werden soll. Blockierte Instanzen können nicht mehr mit dieser kommunizieren."
muteAndBlock: "Stummgeschaltet / Blockiert"
mutedUsers: "Stummgestellte Benutzer"
mutedUsers: "Stummgeschaltete Benutzer"
blockedUsers: "Blockierte Benutzer"
noUsers: "Keine Benutzer"
editProfile: "Profil bearbeiten"
@@ -263,7 +263,8 @@ copyUrl: "URL kopieren"
rename: "Umbenennen"
avatar: "Profilbild"
banner: "Banner"
nsfw: "Dieser Inhalt ist NSFW"
nsfw: "NSFW"
whenServerDisconnected: "Bei Verbindungsverlust zum Server"
disconnectedFromServer: "Verbindung zum Server wurde getrennt"
reload: "Aktualisieren"
doNothing: "Ignorieren"
@@ -364,7 +365,6 @@ unregister: "Deaktivieren"
passwordLessLogin: "Passwortloses Anmelden einrichten"
resetPassword: "Passwort zurücksetzen"
newPasswordIs: "Das neue Passwort ist \"{password}\""
autoReloadWhenDisconnected: "Automatisch aktualisieren wenn die Serververbindung getrennt wird"
autoNoteWatch: "Notizen automatisch beobachten"
autoNoteWatchDescription: "Werde über Notizen, auf die du reagiert oder geantwortet hast, informiert"
reduceUiAnimation: "Animationen der Benutzeroberfläche reduzieren"
@@ -468,6 +468,7 @@ objectStorageUseSSL: "SSL verwenden"
objectStorageUseSSLDesc: "Deaktiviere dies falls du für die API-Verbindungen kein HTTPS verwenden wirst"
objectStorageUseProxy: "Über Proxy verbinden"
objectStorageUseProxyDesc: "Deaktiviere dies falls du keinen Proxy für den Objektspeicher verwenden wirst"
objectStorageSetPublicRead: "Bei Upload auf \"public-read\" stellen"
serverLogs: "Serverprotokolle"
deleteAll: "Alle löschen"
showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen"
@@ -556,6 +557,33 @@ testEmail: "Email-Versand testen"
wordMute: "Wort-Stummschaltung"
userSaysSomething: "{name} hat etwas gesagt."
makeActive: "Aktivieren"
display: "Anzeige"
copy: "Kopieren"
metrics: "Metriken"
overview: "Übersicht"
logs: "Logs"
delayed: "Verzögert"
database: "Datenbank"
channel: "Kanal"
create: "Erstellen"
_serverDisconnectedBehavior:
reload: "Automatisch aktualisieren"
dialog: "Warnungsfenster zeigen"
quiet: "Unaufdringlich warnen"
_channel:
create: "Kanal erstellen"
edit: "Kanal bearbeiten"
setBanner: "Kanalbanner festlegen"
removeBanner: "Kanalbanner entfernen"
featured: "Trends"
owned: "Besitzer"
following: "Folgt"
usersCount: "{n} Teilnehmer"
notesCount: "{n} Notizen"
_sidebar:
full: "Voll"
icon: "Profilbild"
hide: "Ausblenden"
_wordMute:
muteWords: "Wort stummschalten"
muteWordsDescription: "Mit Leerzeichen für eine \"UND\"-Verknüpfung trennen, durch Zeilenumbrüche für eine \"ODER\"-Verknüpfung trennen."
@@ -564,6 +592,7 @@ _wordMute:
hardDescription: "Verhindern, dass Notizen, die die eingestellten Konditionen erfüllen, der Chronik hinzugefügt werden. Zudem werden diese Notizen auch nicht der Chronik hinzugefügt, falls die Konditionen geändert werden."
soft: "Leicht"
hard: "Schwer"
mutedNotes: "Stummgeschaltete Notizen"
_theme:
explore: "Themen erforschen"
install: "Thema installieren"
@@ -642,6 +671,7 @@ _sfx:
chat: "Chat"
chatBg: "Nachrichten (Hintergrund)"
antenna: "Antennen"
channel: "Kanalbenachrichtigung"
_ago:
unknown: "Unbekannt"
future: "Zukunft"
@@ -707,9 +737,9 @@ _permissions:
"write:mutes": "Stummschaltungen bearbeiten"
"write:notes": "Notizen schreiben oder löschen"
"read:notifications": "Benachrichtigungen lesen"
"write:notifications": "Mit Benachrichtigungen arbeiten"
"write:notifications": "Benachrichtigungen bearbeiten"
"read:reactions": "Reaktionen lesen"
"write:reactions": "Reaktionen hinzufügen und bearbeiten"
"write:reactions": "Reaktionen hinzufügen und ändern"
"write:votes": "In Umfragen abstimmen"
"read:pages": "Deine Seiten lesen"
"write:pages": "Deine Seiten bearbeiten oder löschen"
@@ -717,6 +747,8 @@ _permissions:
"write:page-likes": "Liste der Seiten, die mir gefallen, bearbeiten"
"read:user-groups": "Benutzergruppen lesen"
"write:user-groups": "Benutzergruppen bearbeiten oder löschen"
"read:channels": "Kanäle lesen"
"write:channels": "Kanäle bearbeiten"
_auth:
shareAccess: "Möchtest du \"{name}\" authorisieren, auf dieses Benuzerkonto zugreifen zu können?"
shareAccessAsk: "Bist du dir sicher, dass du diese Anwendung authorisieren möchtest, auf dein Benutzerkonto zugreifen zu können?"
@@ -791,6 +823,7 @@ _visibility:
_postForm:
replyPlaceholder: "Dieser Notiz antworten..."
quotePlaceholder: "Diese Notiz zitieren..."
channelPlaceholder: "In einen Kanal senden"
_placeholders:
a: "Was machst du momentan?"
b: "Was ist um dich herum los?"

View File

@@ -264,6 +264,7 @@ rename: "Rename"
avatar: "Avatar"
banner: "Banner"
nsfw: "NSFW"
whenServerDisconnected: "When losing connection to the server"
disconnectedFromServer: "Connection to the server was interrupted."
reload: "Refresh"
doNothing: "Ignore"
@@ -364,7 +365,6 @@ unregister: "Unregister"
passwordLessLogin: "Set up password-less login"
resetPassword: "Reset password"
newPasswordIs: "The new password is \"{password}\""
autoReloadWhenDisconnected: "Auto refresh when disconnected from server"
autoNoteWatch: "Watch note automatically"
autoNoteWatchDescription: "Get notified about the notes which you reactioned or replied."
reduceUiAnimation: "Reduce UI animation"
@@ -468,6 +468,7 @@ objectStorageUseSSL: "Use SSL"
objectStorageUseSSLDesc: "Turn off this if you are not going to use HTTPS for API connection"
objectStorageUseProxy: "Connect over Proxy"
objectStorageUseProxyDesc: "Turn off this if you are not going to use Proxy for ObjectStorage connection"
objectStorageSetPublicRead: "Set \"public-read\" on upload"
serverLogs: "Server logs"
deleteAll: "Delete all"
showFixedPostForm: "Display the posting form at the top of the timeline"
@@ -556,6 +557,33 @@ testEmail: "Test email delivery"
wordMute: "Word mute"
userSaysSomething: "{name} said something"
makeActive: "Activate"
display: "Display"
copy: "Copy"
metrics: "Metrics"
overview: "Overview"
logs: "Logs"
delayed: "Delayed"
database: "Database"
channel: "Channel"
create: "Create"
_serverDisconnectedBehavior:
reload: "Automatically reload"
dialog: "Show warning dialog"
quiet: "Show unobtrusive warning"
_channel:
create: "Create channel"
edit: "Edit channel"
setBanner: "Set banner"
removeBanner: "Remove banner"
featured: "Trending"
owned: "Owner"
following: "Following"
usersCount: "{n} Participants"
notesCount: "{n} Notes"
_sidebar:
full: "Full"
icon: "Avatar"
hide: "Hide"
_wordMute:
muteWords: "Word to mute"
muteWordsDescription: "Separate with spaces for AND condition. Separate with line breaks for OR."
@@ -564,6 +592,7 @@ _wordMute:
hardDescription: "Prevent notes fulfilling the set conditions from being added to the timeline. In addition, these notes will not be added to the timeline even if the conditions are changed."
soft: "Soft"
hard: "Hard"
mutedNotes: "Muted notes"
_theme:
explore: "Explore Themes"
install: "Install theme"
@@ -642,6 +671,7 @@ _sfx:
chat: "Messaging"
chatBg: "Messaging (Background)"
antenna: "Antenna Reception"
channel: "Channel notifications"
_ago:
unknown: "Unknown"
future: "Future"
@@ -717,6 +747,8 @@ _permissions:
"write:page-likes": "Edit likes on pages"
"read:user-groups": "View user groups"
"write:user-groups": "Edit or delete user groups"
"read:channels": "Read channels"
"write:channels": "Modify channels"
_auth:
shareAccess: "Would you like to authorize \"{name}\" to access this account?"
shareAccessAsk: "Are you sure you want to authorize this application to access your account?"
@@ -791,6 +823,7 @@ _visibility:
_postForm:
replyPlaceholder: "Reply to this note..."
quotePlaceholder: "Quote this note..."
channelPlaceholder: "Post to channel"
_placeholders:
a: "What are you up to?"
b: "What's happening around you?"

View File

@@ -264,6 +264,7 @@ rename: "Renombrar"
avatar: "Avatar"
banner: "Banner"
nsfw: "Marcado como sensible"
whenServerDisconnected: "Cuando se pierda la conexión con el servidor"
disconnectedFromServer: "Desconectado del servidor"
reload: "Recargar"
doNothing: "No hacer nada"
@@ -364,7 +365,6 @@ unregister: "Cancelar registro"
passwordLessLogin: "Iniciar sesión sin contraseña"
resetPassword: "Resetear contraseña"
newPasswordIs: "La nueva contraseña es \"{password}\""
autoReloadWhenDisconnected: "Recargar automáticamente cuando el servidor está desconectado"
autoNoteWatch: "Ver nota automáticamente"
autoNoteWatchDescription: "Recibe notificaciones sobre las notas de otros usuarios que a los que respondiste y reaccionaste"
reduceUiAnimation: "Reducir la animación de la UI"
@@ -468,6 +468,7 @@ objectStorageUseSSL: "Usar SSL"
objectStorageUseSSLDesc: "Desactive esto si no va a usar HTTPS para la conexión API"
objectStorageUseProxy: "Conectarse a través de Proxy"
objectStorageUseProxyDesc: "Desactive esto si no va a usar Proxy para la conexión de Almacenamiento de objetos"
objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
serverLogs: "Registros del servidor"
deleteAll: "Eliminar todos"
showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo"
@@ -556,6 +557,33 @@ testEmail: "Prueba de envío"
wordMute: "Silenciar palabras"
userSaysSomething: "{name} dijo algo"
makeActive: "Activar"
display: "Apariencia"
copy: "Copiar"
metrics: "Métricas"
overview: "Resumen"
logs: "Registros"
delayed: "atrasado"
database: "Base de datos"
channel: "Canal"
create: "Crear"
_serverDisconnectedBehavior:
reload: "Recargar automáticamente"
dialog: "Mostrar diálogo de advertencia"
quiet: "Advertencia discreta"
_channel:
create: "Crear canal"
edit: "Editar canal"
setBanner: "Elegir banner"
removeBanner: "Borrar banner"
featured: "Tendencias"
owned: "Dueño"
following: "Siguiendo"
usersCount: "{n} participantes"
notesCount: "{n} notas"
_sidebar:
full: "Completo"
icon: "Avatar"
hide: "Ocultar"
_wordMute:
muteWords: "Palabras que silenciar"
muteWordsDescription: "Separar con espacios indica una declaracion And, separar con lineas nuevas indica una declaracion Or。"
@@ -564,6 +592,7 @@ _wordMute:
hardDescription: "Evitar que se agreguen a la linea de tiempo las notas que cumplen las condiciones. Las notas no agregadas seguirán quitadas aunque cambien las condiciones."
soft: "Suave"
hard: "Duro"
mutedNotes: "Notas silenciadas"
_theme:
explore: "Explorar temas"
install: "Instalar tema"
@@ -642,6 +671,7 @@ _sfx:
chat: "Chat"
chatBg: "Chat (Fondo)"
antenna: "Antena receptora"
channel: "Notificaciones del canal"
_ago:
unknown: "Desconocido"
future: "Futuro"
@@ -717,6 +747,8 @@ _permissions:
"write:page-likes": "Administrar páginas que te gustan"
"read:user-groups": "Ver grupos de usuarios"
"write:user-groups": "Administrar grupos de usuarios"
"read:channels": "Ver canal"
"write:channels": "Modificar canal"
_auth:
shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?"
shareAccessAsk: "¿Está seguro de que desea autorizar esta aplicación para acceder a su cuenta?"
@@ -791,6 +823,7 @@ _visibility:
_postForm:
replyPlaceholder: "Responder a esta nota"
quotePlaceholder: "Citar esta nota"
channelPlaceholder: "Postear en el canal"
_placeholders:
a: "¿Qué haces?"
b: "¿Te pasó algo?"

View File

@@ -364,7 +364,6 @@ unregister: "Se désinscrire"
passwordLessLogin: "Connectez-vous sans mot de passe"
resetPassword: "Réinitialiser mot de passe"
newPasswordIs: "Votre nouveau mot de passe est \"{password}\""
autoReloadWhenDisconnected: "Rechargement automatique lorsque le serveur se déconnecte"
autoNoteWatch: "Surveiller les notes automatiquement"
autoNoteWatchDescription: "Soyez informé des notes auxquelles vous avez réagi ou répondu."
reduceUiAnimation: "Réduire les animations dans linterface"
@@ -540,9 +539,26 @@ pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations d
notificationType: "Type de notifications"
edit: "Editer"
emailConfig: "Configuration du serveur email"
enableEmail: "Activer la distribution de courriel"
emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas doubli."
email: "Adresse de courrier électronique"
smtpConfig: "Paramètres du serveur SMTP"
smtpHost: "Hôte"
smtpPort: "Port"
smtpUser: "Nom dutilisateur·rice"
smtpPass: "Mot de passe"
emptyToDisableSmtpAuth: "Laisser le nom dutilisateur et le mot de passe vides pour désactiver la vérification SMTP"
smtpSecure: "Utiliser SSL/TLS implicitement dans les connexions SMTP"
wordMute: "Filtre de mots"
userSaysSomething: "{name} a dit quelque chose"
makeActive: "Activer"
display: "Affichage"
_channel:
featured: "Tendances"
_sidebar:
full: "Complet"
icon: "Avatar"
hide: "Masquer"
_theme:
explore: "Explorer les thèmes"
install: "Installer un thème"
@@ -1158,6 +1174,9 @@ _deck:
alwaysShowMainColumn: "Toujours afficher la colonne principale"
columnAlign: "Aligner les colonnes"
addColumn: "Ajouter une colonne"
swapLeft: "Déplacer à gauche"
swapRight: "Déplacer à droite"
stackLeft: "Empiler à gauche"
_columns:
widgets: "Widgets"
notifications: "Notifications"

View File

@@ -16,6 +16,9 @@ noNotes: "ノートはありません"
noNotifications: "通知はありません"
instance: "インスタンス"
settings: "設定"
basicSettings: "基本設定"
otherSettings: "その他の設定"
openInWindow: "ウィンドウで開く"
profile: "プロフィール"
timeline: "タイムライン"
noAccountDescription: "自己紹介はありません"
@@ -40,6 +43,7 @@ deleteAndEditConfirm: "このノートを削除してもう一度編集します
addToList: "リストに追加"
sendMessage: "メッセージを送信"
copyUsername: "ユーザー名をコピー"
searchUser: "ユーザーを検索"
reply: "返信"
loadMore: "もっと見る"
youGotNewFollower: "フォローされました"
@@ -66,8 +70,11 @@ followers: "フォロワー"
followsYou: "フォローされています"
createList: "リスト作成"
manageLists: "リストの管理"
error: "問題が発生しました"
error: "エラー"
somethingHappened: "問題が発生しました"
retry: "再試行"
pageLoadError: "ページの読み込みに失敗しました。"
pageLoadErrorDescription: "これは通常、ネットワークまたはブラウザキャッシュが原因です。キャッシュをクリアするか、しばらく待ってから再度試してください。"
enterListName: "リスト名を入力"
privacy: "プライバシー"
makeFollowManuallyApprove: "フォローを承認制にする"
@@ -106,6 +113,8 @@ unsuspendConfirm: "解凍しますか?"
selectList: "リストを選択"
selectAntenna: "アンテナを選択"
selectWidget: "ウィジェットを選択"
editWidgets: "ウィジェットを編集"
editWidgetsExit: "編集を終了"
customEmojis: "カスタム絵文字"
emoji: "絵文字"
emojiName: "絵文字名"
@@ -177,7 +186,6 @@ processing: "処理中"
preview: "プレビュー"
default: "デフォルト"
noCustomEmojis: "絵文字はありません"
customEmojisOfRemote: "リモートの絵文字"
noJobs: "ジョブはありません"
federating: "連合中"
blocked: "ブロック中"
@@ -264,6 +272,7 @@ rename: "名前を変更"
avatar: "アイコン"
banner: "バナー"
nsfw: "閲覧注意"
whenServerDisconnected: "サーバーとの接続が失われたとき"
disconnectedFromServer: "サーバーから切断されました"
reload: "リロード"
doNothing: "なにもしない"
@@ -364,7 +373,6 @@ unregister: "登録を解除"
passwordLessLogin: "パスワード無しログイン"
resetPassword: "パスワードをリセット"
newPasswordIs: "新しいパスワードは「{password}」です"
autoReloadWhenDisconnected: "サーバー切断時に自動リロード"
autoNoteWatch: "ノートの自動ウォッチ"
autoNoteWatchDescription: "あなたがリアクションしたり返信したりした他のユーザーのノートに関する通知を受け取るようにします。"
reduceUiAnimation: "UIのアニメーションを減らす"
@@ -445,7 +453,7 @@ total: "合計"
weekOverWeekChanges: "前週比"
dayOverDayChanges: "前日比"
appearance: "アピアランス"
clinetSettings: "クライアント設定"
clientSettings: "クライアント設定"
accountSettings: "アカウント設定"
promotion: "プロモーション"
promote: "プロモート"
@@ -468,6 +476,7 @@ objectStorageUseSSL: "SSLを使用する"
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
objectStorageUseProxy: "Proxyを利用する"
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
serverLogs: "サーバーログ"
deleteAll: "全て削除"
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
@@ -475,6 +484,8 @@ newNoteRecived: "新しいノートがあります"
sounds: "サウンド"
listen: "聴く"
none: "なし"
showInPage: "ページで表示"
popout: "ポップアウト"
volume: "音量"
details: "詳細"
chooseEmoji: "絵文字を選択"
@@ -517,7 +528,6 @@ enableInfiniteScroll: "自動でもっと見る"
visibility: "公開範囲"
poll: "アンケート"
useCw: "内容を隠す"
fixedWidgetsPosition: "ウィジェットの位置を固定する"
enablePlayer: "プレイヤーを開く"
disablePlayer: "プレイヤーを閉じる"
expandTweet: "ツイートを展開する"
@@ -556,6 +566,46 @@ testEmail: "配信テスト"
wordMute: "ワードミュート"
userSaysSomething: "{name}が何かを言いました"
makeActive: "アクティブにする"
display: "表示"
copy: "コピー"
metrics: "メトリクス"
overview: "概要"
logs: "ログ"
delayed: "遅延"
database: "データベース"
channel: "チャンネル"
create: "作成"
notificationSetting: "通知設定"
notificationSettingDesc: "表示する通知の種別を選択してください。"
useGlobalSetting: "グローバル設定を使う"
useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。"
other: "その他"
regenerateLoginToken: "ログイントークンを再生成"
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
fileIdOrUrl: "ファイルIDまたはURL"
chatOpenBehavior: "チャットを開くときの動作"
_serverDisconnectedBehavior:
reload: "自動でリロード"
dialog: "ダイアログで警告"
quiet: "控えめに警告"
_channel:
create: "チャンネルを作成"
edit: "チャンネルを編集"
setBanner: "バナーを設定"
removeBanner: "バナーを削除"
featured: "トレンド"
owned: "管理中"
following: "フォロー中"
usersCount: "{n}人が参加中"
notesCount: "{n}投稿があります"
_sidebar:
full: "フル"
icon: "アイコン"
hide: "隠す"
_wordMute:
muteWords: "ミュートするワード"
@@ -565,6 +615,7 @@ _wordMute:
hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。"
soft: "ソフト"
hard: "ハード"
mutedNotes: "ミュートされたノート"
_theme:
explore: "テーマを探す"
@@ -646,6 +697,7 @@ _sfx:
chat: "チャット"
chatBg: "チャット(バックグラウンド)"
antenna: "アンテナ受信"
channel: "チャンネル通知"
_ago:
unknown: "謎"
@@ -726,6 +778,8 @@ _permissions:
"write:page-likes": "ページのいいねを操作する"
"read:user-groups": "ユーザーグループを見る"
"write:user-groups": "ユーザーグループを操作する"
"read:channels": "チャンネルを見る"
"write:channels": "チャンネルを操作する"
_auth:
shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?"
@@ -763,6 +817,7 @@ _widgets:
photos: "フォト"
digitalClock: "デジタル時計"
federation: "連合"
postForm: "投稿フォーム"
_cw:
hide: "隠す"
@@ -808,6 +863,7 @@ _visibility:
_postForm:
replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..."
channelPlaceholder: "チャンネルに投稿..."
_placeholders:
a: "いまどうしてる?"
b: "何かありましたか?"
@@ -1249,8 +1305,11 @@ _notification:
renote: "Renote"
quote: "引用"
reaction: "リアクション"
pollVote: "投票"
receiveFollowRequest: "フォローリクエスト"
pollVote: "アンケートに投票された"
receiveFollowRequest: "フォロー申請を受け取った"
followRequestAccepted: "フォローが受理された"
groupInvited: "グループに招待された"
app: "連携アプリからの通知"
_deck:
alwaysShowMainColumn: "常にメインカラムを表示"

View File

@@ -348,7 +348,6 @@ unregister: "登録やめる"
passwordLessLogin: "パスワード無くてもログインできるようにする"
resetPassword: "パスワードをリセット"
newPasswordIs: "今度のパスワードは「{password}」や"
autoReloadWhenDisconnected: "サーバーが調子悪いときには自動でリロードしたる"
notFoundDescription: "指定されたURLに該当するページはあらへんやった。"
close: "さいなら"
joinedGroups: "参加しとるグループ"
@@ -356,6 +355,8 @@ invites: "来てや"
smtpHost: "ホスト"
smtpUser: "ユーザー名"
smtpPass: "パスワード"
_sidebar:
icon: "アイコン"
_theme:
keys:
renote: "Renote"

View File

@@ -364,7 +364,6 @@ unregister: "등록 해제"
passwordLessLogin: "비밀번호 없이 로그인"
resetPassword: "비밀번호 재설정"
newPasswordIs: "새로운 비밀번호는 \"{password}\" 입니다"
autoReloadWhenDisconnected: "서버와의 연결이 끊기면 자동 새로고침"
autoNoteWatch: "노트를 자동으로 지켜보기"
autoNoteWatchDescription: "리액션하거나 답글을 남긴 다른 유저의 노트에 대한 알림을 받습니다."
reduceUiAnimation: "UI의 애니메이션을 줄이기"
@@ -434,7 +433,7 @@ tags: "태그"
docSource: "이 문서의 소스"
createAccount: "계정 만들기"
existingAcount: "기존 계정"
regenerate: "다시 생성"
regenerate: "생성"
fontSize: "글자 크기"
noFollowRequests: "처리되지 않은 팔로우 요청이 없습니다"
openImageInNewTab: "새 탭에서 이미지 열기"
@@ -533,14 +532,40 @@ generateAccessToken: "액세스 토큰 생성"
permission: "권한"
enableAll: "전체 선택"
disableAll: "전체 해제"
tokenRequested: "계정 접근 허용"
edit: "편집"
useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용"
emailConfig: "메일 서버 설정"
emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다."
email: "메일 주소"
smtpConfig: "SMTP 서버 설정"
smtpHost: "호스트"
smtpPort: "포트"
smtpUser: "유저명"
smtpPass: "비밀번호"
emptyToDisableSmtpAuth: "SMTP 인증을 사용하지 않으려면 공란으로 비워둡니다."
smtpSecure: "SMTP 연결에 Implicit SSL/TTS 사용"
smtpSecureInfo: "STARTTLS 사용 시에는 해제합니다."
wordMute: "단어 뮤트"
makeActive: "활성화"
copy: "복사"
logs: "로그"
database: "데이터베이스"
channel: "채널"
_channel:
create: "채널 생성"
setBanner: "배너 설정"
removeBanner: "배너 삭제"
featured: "트렌드"
following: "팔로잉"
usersCount: "{n}명 참여 중"
notesCount: "{n}노트"
_sidebar:
icon: "아바타"
hide: "숨기기"
_wordMute:
muteWords: "뮤트할 단어"
mutedNotes: "뮤트된 노트"
_theme:
explore: "테마 찾아보기"
install: "테마 설치"
@@ -561,7 +586,10 @@ _theme:
func: "함수"
funcKind: "함수 종류"
argument: "매개변수"
importInfo: "여기에 테마 코드를 붙여 넣어 에디터로 불러올 수 있습니다."
keys:
link: "링크"
hashtag: "해시태그"
mention: "멘션"
renote: "Renote"
divider: "구분선"
@@ -1137,7 +1165,11 @@ _notification:
renote: "Renote"
quote: "인용"
reaction: "리액션"
receiveFollowRequest: "팔로우 요청"
_deck:
alwaysShowMainColumn: "메인 칼럼 항상 표시"
columnAlign: "칼럼 정렬"
addColumn: "칼럼 추가"
swapLeft: "왼쪽으로 이동"
swapRight: "오른쪽으로 이동"
swapUp: "위로 이동"

View File

@@ -264,6 +264,7 @@ rename: "重命名"
avatar: "头像"
banner: "Banner"
nsfw: "阅读注意"
whenServerDisconnected: "与服务器连接中断时"
disconnectedFromServer: "已从服务器断开连接"
reload: "重新加载"
doNothing: "什么都不做"
@@ -358,13 +359,12 @@ moderator: "版主"
nUsersMentioned: "{n} 被提到"
securityKey: "安全密钥"
securityKeyName: "密钥名称"
registerSecurityKey: "注册安全密钥"
registerSecurityKey: "注册硬件安全密钥"
lastUsed: "最后使用:"
unregister: "删除账户"
passwordLessLogin: "无密码登录"
resetPassword: "重置密码"
newPasswordIs: "新的密码是「{password}」"
autoReloadWhenDisconnected: "断开连接时自动重新加载"
autoNoteWatch: "自动关注帖子"
autoNoteWatchDescription: "让您能够收到关于「回应」和回复其他用户的帖子的通知。"
reduceUiAnimation: "减少UI动画"
@@ -455,19 +455,20 @@ showFeaturedNotesInTimeline: "在时间线上显示热门推荐"
objectStorage: "对象存储"
useObjectStorage: "使用对象存储"
objectStorageBaseUrl: "基本网址"
objectStorageBaseUrlDesc: "供参考的URL。如果使用CDN或Proxy则其URL为S3\"https://<bucket>.s3.amazonaws.com\"、GCS等\"https://storage-googleapis.proxy.ustclug.org/<bucket>\"。"
objectStorageBaseUrlDesc: "URL前缀用于构造URL到对象媒体的引用如果使用的是CDN或反向代理请指定其URL否则请根据您使用的服务指定可公开访问的地址。例如“https://<bucket>.s3.amazonaws.com”用于AWS S3https://storage.googleapis.com/<bucket>”用于GCS"
objectStorageBucket: "存储桶"
objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。"
objectStoragePrefix: "前缀"
objectStoragePrefixDesc: "文件将存储在此前缀的目录下。"
objectStorageEndpoint: "端点"
objectStorageEndpointDesc: "S3默认情况下为空否则请为每个服务指定端点。 指定为“<host>”或“<host>:<port>”。"
objectStorageEndpointDesc: "如果你希望使用AWS S3请留空。否则请根据你使用的服务来进行设置指定端点形式为“<host>”或“<host>:<port>”。"
objectStorageRegion: "可用区"
objectStorageRegionDesc: "指定一个可用区例如“xx-east-1”。 如果您的对象存储服务没有可用区概念请将其留空或填写“us-east-1”。"
objectStorageUseSSL: "使用SSL"
objectStorageUseSSLDesc: "如果不使用https进行API连接请关闭。"
objectStorageUseProxy: "使用代理"
objectStorageUseProxyDesc: "如果您不使用代理进行API连接请将其关闭。"
objectStorageSetPublicRead: "上传时设置为public-read"
serverLogs: "服务器日志"
deleteAll: "删除全部"
showFixedPostForm: "在时间线顶部显示帖子表单"
@@ -490,8 +491,8 @@ state: "状态"
sort: "排序"
ascendingOrder: "升序"
descendingOrder: "降序"
scratchpad: "暂存器"
scratchpadDescription: "暂存器为AiScript提供了实验环境。您可以编写代码以与Misskey交互运行它并查看结果。"
scratchpad: "便签本"
scratchpadDescription: "便签本为AiScript提供了实验环境。您可以编写代码以与Misskey交互运行它并查看结果。"
output: "输出"
script: "脚本"
disablePagesScript: "禁用页面脚本"
@@ -556,12 +557,42 @@ testEmail: "邮件发送测试"
wordMute: "文字屏蔽"
userSaysSomething: "{name}说了什么"
makeActive: "激活"
display: "显示"
copy: "复制"
metrics: "指标"
overview: "概述"
logs: "日志"
delayed: "延迟"
database: "数据库"
channel: "频道"
create: "创建"
_serverDisconnectedBehavior:
reload: "自动重载"
dialog: "对话框警告"
quiet: "安静警告"
_channel:
create: "创建频道"
edit: "编辑频道"
setBanner: "设置横幅"
removeBanner: "删除横幅"
featured: "热点"
owned: "管理中"
following: "正在关注"
usersCount: "有{n}人参与"
notesCount: "有{n}个帖子"
_sidebar:
full: "全部"
icon: "图标"
hide: "隐藏"
_wordMute:
muteWords: "禁用词"
muteWordsDescription: "使用空格分隔表示AND逻辑使用换行符分隔表示OR逻辑。"
muteWordsDescription2: "将关键字用斜线括起来表示正则表达式。"
softDescription: "隐藏时间轴中指定条件的帖文。"
hardDescription: "防止将具有指定条件的帖文添加到时间线。 即使您更改条件,未添加的帖文也会被排除在外。"
soft: "软屏蔽"
hard: "硬屏蔽"
mutedNotes: "被屏蔽的帖文"
_theme:
explore: "寻找主题"
install: "安装主题"
@@ -621,7 +652,7 @@ _theme:
cwFg: "CW 按钮文本"
cwHoverBg: "CW 按钮背景(悬停)"
toastBg: "吐司提示背景"
toastFg: "司提示文本"
toastFg: "司提示文本"
buttonBg: "按钮背景"
buttonHoverBg: "按钮背景(悬停)"
inputBorder: "输入框边框"
@@ -640,6 +671,7 @@ _sfx:
chat: "聊天"
chatBg: "聊天背景"
antenna: "天线接收"
channel: "频道通知"
_ago:
unknown: "未知"
future: "未来"
@@ -676,7 +708,7 @@ _tutorial:
step6_1: "现在,您将可以在时间线上看到其他用户的帖子。"
step6_2: "您还可以在其他人的帖子上进行「回应」,以快速做出简单回复。"
step6_3: "在他人的贴子上按下「+」图标,即可选择想要的表情来进行「回应」。"
step7_1: "对Misskey基本操作的简单介绍到此结束了。 辛苦了!"
step7_1: "对Misskey基本操作的简单介绍到此结束了。 辛苦了!"
step7_2: "如果你想了解更多有关Misskey的信息请参见{help}。"
step7_3: "接下来享受Misskey带来的乐趣吧🚀"
_2fa:
@@ -687,7 +719,7 @@ _2fa:
step2: "然后,扫描屏幕上显示的二维码。"
step3: "输入您的应用提供的动态口令以完成设置。"
step4: "从现在开始,任何登录操作都将要求您提供动态口令。"
securityKeyInfo: "您可以设置使用支持FIDO2的硬件安全密钥、指纹或设备上的PIN来保护您的登录过程。"
securityKeyInfo: "您可以设置使用支持FIDO2的硬件安全密钥、设备上的指纹或PIN来保护您的登录过程。"
_permissions:
"read:account": "查看账户信息"
"write:account": "更改帐户信息"
@@ -715,6 +747,8 @@ _permissions:
"write:page-likes": "操作喜欢的页面"
"read:user-groups": "查看用户组"
"write:user-groups": "操作用户组"
"read:channels": "查看频道"
"write:channels": "管理频道"
_auth:
shareAccess: "您要授权允许“{name}”访问您的帐户吗?"
shareAccessAsk: "您确定要授权此应用程序访问您的帐户吗?"
@@ -789,6 +823,7 @@ _visibility:
_postForm:
replyPlaceholder: "回复这个帖子..."
quotePlaceholder: "引用这个帖子..."
channelPlaceholder: "发布到频道…"
_placeholders:
a: "现在如何?"
b: "发生了什么?"

View File

@@ -47,6 +47,7 @@ receiveFollowRequest: "收到追隨請求"
followRequestAccepted: "追隨請求已接受"
mention: "提及"
mentions: "提及"
directNotes: "私信"
importAndExport: "匯入 / 匯出"
import: "匯入"
export: "匯出"
@@ -103,17 +104,22 @@ unblockConfirm: "確定解除封鎖此用戶?"
suspendConfirm: "確定凍結此帳號?"
unsuspendConfirm: "確定解凍此帳號?"
selectList: "選擇清單"
selectAntenna: "選擇天線"
selectWidget: "選擇小工具"
customEmojis: "自訂表情符號"
emoji: "表情符號"
emojiName: "表情符號名稱"
emojiUrl: "表情符號URL"
addEmoji: "新增表情符號"
settingGuide: "推薦設定"
cacheRemoteFiles: "遠程文件緩存"
cacheRemoteFilesDescription: "如果禁用此設定,遠程文件將會被直接連結而非緩存。禁用將節省服務器上的存儲空間,但會因為沒有生成預覽圖而增加流量。"
flagAsBot: "此帳戶是Bot"
flagAsCat: "此帳戶是Cat"
autoAcceptFollowed: "自動許可追隨"
addAcount: "新增帳號"
loginFailed: "登入失敗"
showOnRemote: "轉到所在實例顯示"
general: "一般"
wallpaper: "桌布"
setWallpaper: "設定桌布"
@@ -122,12 +128,16 @@ searchWith: "搜尋: {q}"
youHaveNoLists: "沒有任何清單"
followConfirm: "你真的要關注{name}嗎?"
proxyAccount: "代理帳號"
proxyAccountDescription: "代理帳號是在某些情況下充當其他服務器用戶的帳號。例如,當用戶將一個來自其他服務器的帳號放在列表中時,由於沒有其他用戶關注該帳號,該指令不會傳送到該服務器上,因此會由代理帳戶關注。"
host: "主機"
selectUser: "選取使用者"
recipient: "發送至"
annotation: "註解"
federation: "聯邦宇宙"
instances: "實例"
registeredAt: "初次觀測"
latestRequestSentAt: "上次發送的請求"
latestRequestReceivedAt: "上次收到的請求"
latestStatus: "最後狀態"
storageUsage: "已使用容量"
charts: "圖表"
@@ -148,6 +158,7 @@ instanceInfo: "實例資訊"
statistics: "統計"
clearQueue: "清除佇列"
clearQueueConfirmTitle: "確定要清除佇列嗎?"
clearQueueConfirmText: "未發佈的帖子將不會發佈。您通常不需要確認。"
clearCachedFiles: "清除快取資料"
clearCachedFilesConfirm: "確定要清除緩存資料嗎?"
blockedInstances: "已封鎖的實例"
@@ -200,6 +211,8 @@ upload: "上傳"
fromDrive: "從雲端"
fromUrl: "從URL"
uploadFromUrl: "從網址上傳"
uploadFromUrlDescription: "您要上傳的文件的URL"
uploadFromUrlRequested: "已請求上傳"
uploadFromUrlMayTakeTime: "還需要一些時間才能完成上傳。"
explore: "探索"
games: "Misskey 遊戲"
@@ -210,6 +223,7 @@ nUsersRead: "{n}人已讀"
tos: "使用條款"
start: "開始"
home: "首頁"
remoteUserCaution: "由於是遠程用戶,信息不完整。"
activity: "動態"
images: "圖片"
birthday: "生日"
@@ -217,10 +231,13 @@ yearsOld: "{age}歲"
registeredDate: "註冊日期"
location: "位置"
theme: "外觀主題"
themeForLightMode: "在淺色模式下使用的主題"
themeForDarkMode: "在深色模式下使用的主題"
light: "淺色"
dark: "灰暗"
lightThemes: "明亮主題"
darkThemes: "灰暗主題"
syncDeviceDarkMode: "將深色模式與設備設置同步"
drive: "雲端硬碟"
fileName: "檔案名稱"
selectFile: "選擇檔案"
@@ -238,11 +255,14 @@ emptyFolder: "空的資料夾"
unableToDelete: "無法刪除"
inputNewFileName: "輸入檔案名稱"
inputNewFolderName: "輸入新資料夾的名稱"
circularReferenceFolder: "目標文件夾是您要移動的文件夾的子文件夾。"
hasChildFilesOrFolders: "此文件夾不是空的,無法刪除。"
copyUrl: "複製URL"
rename: "重新命名"
avatar: "頭像"
banner: "橫幅"
nsfw: "敏感內容"
whenServerDisconnected: "與服務器的連接中斷時"
disconnectedFromServer: "與伺服器中斷連線"
reload: "重新載入"
doNothing: "無視"
@@ -269,10 +289,12 @@ connectSerice: "連線"
disconnectSerice: "中斷連線"
enableLocalTimeline: "開啟本地時間軸"
enableGlobalTimeline: "開啟全球時間軸"
disablingTimelinesInfo: "即使您禁用了時間線功能,管理員和協調人仍可以繼續使用,以方便您。"
registration: "註冊"
enableRegistration: "開啟新用戶註冊"
invite: "邀請"
proxyRemoteFiles: "代理遠程檔案"
proxyRemoteFilesDescription: "啟用此設置後,由於超出存儲容量而未保存或刪除的遠程文件將被本地代理,並且將生成預覽圖。這不影響服務器的存儲。"
driveCapacityPerLocalAccount: "每個本地用戶的雲端容量"
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量"
inMb: "以Mbps為單位"
@@ -280,6 +302,7 @@ iconUrl: "圖像URL"
bannerUrl: "橫幅圖片URL"
basicInfo: "基本資訊"
pinnedUsers: "置頂用戶"
pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的用戶。"
hcaptcha: "hCaptcha"
enableHcaptcha: "啟用 hCaptcha"
hcaptchaSiteKey: "網站金鑰"
@@ -288,13 +311,21 @@ recaptcha: "reCAPTCHA"
enableRecaptcha: "啟用 reCAPTCHA"
recaptchaSiteKey: "網站金鑰"
recaptchaSecretKey: "金鑰"
avoidMultiCaptchaConfirm: "使用多種驗證方式可能會造成干擾,您要禁用其他驗證方式嗎?您可以按“取消”保留多種驗證方式。"
antennas: "天線"
manageAntennas: "管理天線"
name: "名稱"
antennaSource: "接收來源"
antennaKeywords: "包含的關鍵字"
antennaExcludeKeywords: "排除關鍵字"
antennaKeywordsDescription: "用空格分隔指定AND、用換行符分隔指定OR"
notifyAntenna: "通知我有新的貼文"
serviceworker: "ServiceWorker"
enableServiceworker: "開啟 ServiceWorker"
antennaUsersDescription: "指定用換行符分隔的用戶名"
caseSensitive: "區分大小寫"
withReplies: "包含回覆"
connectedTo: "您的帳號已連接到以下社交帳號"
notesAndReplies: "貼文與回覆"
withFiles: "附件"
silence: "禁言"
@@ -311,6 +342,9 @@ popularTags: "熱門標籤"
userList: "清單"
about: "資訊"
aboutMisskey: "關於 Misskey"
aboutMisskeyText: "Misskey是由syuilo於2014年開發的開放源代碼軟件。"
misskeyMembers: "現在由以下成員開發及維護:"
misskeySource: "源代碼在這裡公開:"
misskeyTranslation: "幫助我們為Misskey的翻譯工作出一分力"
misskeyDonate: "向Misskey捐款以支援我們開發工作"
morePatrons: "感激你們的支持、 幫助。 🥰"
@@ -328,7 +362,6 @@ unregister: "刪除賬戶"
passwordLessLogin: "設置無密碼登入"
resetPassword: "重置密碼"
newPasswordIs: "新密碼為「{password}」"
autoReloadWhenDisconnected: "和伺服器斷線時自動重新載入"
autoNoteWatch: "自動追隨貼文"
autoNoteWatchDescription: "收到反應或回覆過的貼文的通知"
reduceUiAnimation: "減少介面的動態視覺"
@@ -346,6 +379,7 @@ close: "關閉"
group: "群組"
groups: "群組"
createGroup: "創建群組"
ownedGroups: "擁有的群組"
joinedGroups: "群組成員"
invites: "邀請"
groupName: "群組名稱"
@@ -379,10 +413,23 @@ normalPassword: "密碼強度普通"
strongPassword: "密碼強度堅強"
passwordMatched: "密碼一致"
passwordNotMatched: "密碼不一致"
signinWith: "以{x}登錄"
signinFailed: "登入失敗。 請檢查用戶名和密碼。"
tapSecurityKey: "點擊安全密鑰"
or: "或者"
uiLanguage: "介面語言"
groupInvited: "您有新的群組邀請"
aboutX: "關於{x}"
useOsNativeEmojis: "使用OS原生表情符號"
youHaveNoGroups: "找不到群組"
joinOrCreateGroup: "請加入現有群組,或創建新群組。"
noHistory: "沒有歷史紀錄"
disableAnimatedMfm: "禁用MFM動畫"
doing: "正在進行"
category: "類別"
tags: "標籤"
docSource: "文件來源"
createAccount: "建立帳戶"
fontSize: "字體大小"
total: "合計"
clinetSettings: "用戶端設定"
@@ -404,21 +451,82 @@ scratchpad: "暫存記憶體"
output: "輸出"
deleteAllFiles: "刪除所有檔案"
deleteAllFilesConfirm: "要删除所有檔案吗?"
userSuspended: "該用戶已被凍結"
userSilenced: "該用戶已被禁言。"
sidebar: "側邊列"
divider: "分割線"
addItem: "新增項目"
rooms: "房間"
relays: "中繼"
addRelay: "添加中繼"
inboxUrl: "私信URL"
addedRelays: "已添加的中繼"
serviceworkerInfo: "您需要啟用推送通知"
deletedNote: "已删除的貼文"
invisibleNote: "隱藏的帖子"
enableInfiniteScroll: "啟用自動滾動頁面模式"
visibility: "公開範圍"
poll: "投票"
useCw: "隱藏內容"
fixedWidgetsPosition: "固定小工具的位置"
enablePlayer: "打開播放器"
disablePlayer: "關閉播放器"
expandTweet: "展開推文"
themeEditor: "主題編輯器"
description: "描述"
author: "作者"
leaveConfirm: "有未保存的更改。要放棄嗎?"
manage: "管理"
plugins: "插件"
pluginInstallWarn: "請不要安裝來源不明的插件。"
deck: "多欄模式"
undeck: "取消多欄模式"
permission: "權限"
enableAll: "啟用全部"
disableAll: "停用全部"
tokenRequested: "允許訪問帳號"
notificationType: "通知形式"
edit: "編輯"
useStarForReactionFallback: "以★代替未知的表情符號"
emailConfig: "電郵服務器設定"
enableEmail: "啟用發送電郵功能"
emailConfigInfo: "用於確認電郵地址及密碼重置"
email: "電郵地址"
smtpConfig: "SMTP服務器設定"
smtpHost: "主機"
smtpPort: "端口"
smtpUser: "使用名稱"
smtpPass: "密碼"
channel: "頻道"
create: "新增"
_serverDisconnectedBehavior:
reload: "自動重載"
dialog: "以對話框警告"
quiet: "適當地警告"
_channel:
create: "建立頻道"
edit: "編輯頻道"
setBanner: "設置封面圖"
removeBanner: "移除封面圖"
featured: "流行"
owned: "管理中"
following: "關注中"
usersCount: "有{n}人參與"
notesCount: "有{n}個帖子"
_sidebar:
icon: "頭像"
_theme:
func: "函数"
keys:
mention: "提及"
renote: "轉發貼文"
divider: "分割線"
_sfx:
note: "貼文"
noteMy: "我的貼文"
notification: "通知"
chat: "傳送訊息"
channel: "頻道通知"
_ago:
unknown: "未知"
future: "未來"
@@ -469,6 +577,8 @@ _permissions:
"read:reactions": "查看反應"
"write:reactions": "編輯反應"
"write:votes": "投票"
"read:channels": "已查看的頻道"
"write:channels": "操作頻道"
_weekday:
sunday: "週日"
monday: "週一"
@@ -495,6 +605,8 @@ _poll:
_visibility:
home: "首頁"
followers: "追隨者"
_postForm:
channelPlaceholder: "發佈到頻道"
_profile:
name: "名稱"
username: "使用名稱"
@@ -673,6 +785,7 @@ _notification:
youGotPoll: "{name}已投票"
youWereFollowed: "您有新的追隨者"
yourFollowRequestAccepted: "您的追隨請求已通過"
youWereInvitedToGroup: "您有新的群組邀請"
_types:
follow: "追隨中"
mention: "提及"
@@ -683,5 +796,6 @@ _deck:
_columns:
notifications: "通知"
tl: "時間軸"
antenna: "天線"
list: "清單"
mentions: "提及"

View File

@@ -0,0 +1,58 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class channel1596548170836 implements MigrationInterface {
name = 'channel1596548170836'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "channel" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastNotedAt" TIMESTAMP WITH TIME ZONE, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "description" character varying(2048), "bannerId" character varying(32), "notesCount" integer NOT NULL DEFAULT 0, "usersCount" integer NOT NULL DEFAULT 0, CONSTRAINT "PK_590f33ee6ee7d76437acf362e39" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_71cb7b435b7c0d4843317e7e16" ON "channel" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_29ef80c6f13bcea998447fce43" ON "channel" ("lastNotedAt") `);
await queryRunner.query(`CREATE INDEX "IDX_823bae55bd81b3be6e05cff438" ON "channel" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_0f58c11241e649d2a638a8de94" ON "channel" ("notesCount") `);
await queryRunner.query(`CREATE INDEX "IDX_094b86cd36bb805d1aa1e8cc9a" ON "channel" ("usersCount") `);
await queryRunner.query(`CREATE TABLE "channel_following" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "followeeId" character varying(32) NOT NULL, "followerId" character varying(32) NOT NULL, CONSTRAINT "PK_8b104be7f7415113f2a02cd5bdd" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_11e71f2511589dcc8a4d3214f9" ON "channel_following" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_0e43068c3f92cab197c3d3cd86" ON "channel_following" ("followeeId") `);
await queryRunner.query(`CREATE INDEX "IDX_6d8084ec9496e7334a4602707e" ON "channel_following" ("followerId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2e230dd45a10e671d781d99f3e" ON "channel_following" ("followerId", "followeeId") `);
await queryRunner.query(`CREATE TABLE "channel_note_pining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "channelId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_44f7474496bcf2e4b741681146d" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_8125f950afd3093acb10d2db8a" ON "channel_note_pining" ("channelId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f36fed37d6d4cdcc68c803cd9c" ON "channel_note_pining" ("channelId", "noteId") `);
await queryRunner.query(`ALTER TABLE "note" ADD "channelId" character varying(32) DEFAULT null`);
await queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId") `);
await queryRunner.query(`ALTER TABLE "channel" ADD CONSTRAINT "FK_823bae55bd81b3be6e05cff4383" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "channel" ADD CONSTRAINT "FK_999da2bcc7efadbfe0e92d3bc19" FOREIGN KEY ("bannerId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_f22169eb10657bded6d875ac8f9" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "channel_following" ADD CONSTRAINT "FK_0e43068c3f92cab197c3d3cd86e" FOREIGN KEY ("followeeId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "channel_following" ADD CONSTRAINT "FK_6d8084ec9496e7334a4602707e1" FOREIGN KEY ("followerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "channel_note_pining" ADD CONSTRAINT "FK_8125f950afd3093acb10d2db8a8" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "channel_note_pining" ADD CONSTRAINT "FK_10b19ef67d297ea9de325cd4502" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "channel_note_pining" DROP CONSTRAINT "FK_10b19ef67d297ea9de325cd4502"`);
await queryRunner.query(`ALTER TABLE "channel_note_pining" DROP CONSTRAINT "FK_8125f950afd3093acb10d2db8a8"`);
await queryRunner.query(`ALTER TABLE "channel_following" DROP CONSTRAINT "FK_6d8084ec9496e7334a4602707e1"`);
await queryRunner.query(`ALTER TABLE "channel_following" DROP CONSTRAINT "FK_0e43068c3f92cab197c3d3cd86e"`);
await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_f22169eb10657bded6d875ac8f9"`);
await queryRunner.query(`ALTER TABLE "channel" DROP CONSTRAINT "FK_999da2bcc7efadbfe0e92d3bc19"`);
await queryRunner.query(`ALTER TABLE "channel" DROP CONSTRAINT "FK_823bae55bd81b3be6e05cff4383"`);
await queryRunner.query(`DROP INDEX "IDX_f22169eb10657bded6d875ac8f"`);
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "channelId"`);
await queryRunner.query(`DROP INDEX "IDX_f36fed37d6d4cdcc68c803cd9c"`);
await queryRunner.query(`DROP INDEX "IDX_8125f950afd3093acb10d2db8a"`);
await queryRunner.query(`DROP TABLE "channel_note_pining"`);
await queryRunner.query(`DROP INDEX "IDX_2e230dd45a10e671d781d99f3e"`);
await queryRunner.query(`DROP INDEX "IDX_6d8084ec9496e7334a4602707e"`);
await queryRunner.query(`DROP INDEX "IDX_0e43068c3f92cab197c3d3cd86"`);
await queryRunner.query(`DROP INDEX "IDX_11e71f2511589dcc8a4d3214f9"`);
await queryRunner.query(`DROP TABLE "channel_following"`);
await queryRunner.query(`DROP INDEX "IDX_094b86cd36bb805d1aa1e8cc9a"`);
await queryRunner.query(`DROP INDEX "IDX_0f58c11241e649d2a638a8de94"`);
await queryRunner.query(`DROP INDEX "IDX_823bae55bd81b3be6e05cff438"`);
await queryRunner.query(`DROP INDEX "IDX_29ef80c6f13bcea998447fce43"`);
await queryRunner.query(`DROP INDEX "IDX_71cb7b435b7c0d4843317e7e16"`);
await queryRunner.query(`DROP TABLE "channel"`);
}
}

View File

@@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class channel21596786425167 implements MigrationInterface {
name = 'channel21596786425167'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "channel_following" ADD "readCursor" TIMESTAMP WITH TIME ZONE NOT NULL`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "channel_following" DROP COLUMN "readCursor"`);
}
}

View File

@@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class objectStorageSetPublicRead1597230137744 implements MigrationInterface {
name = 'objectStorageSetPublicRead1597230137744'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageSetPublicRead" boolean NOT NULL DEFAULT false`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageSetPublicRead"`);
}
}

View File

@@ -0,0 +1,16 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class IncludingNotificationTypes1597236229720 implements MigrationInterface {
name = 'IncludingNotificationTypes1597236229720'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "user_profile_includingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "includingNotificationTypes" "user_profile_includingnotificationtypes_enum" array`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "includingNotificationTypes"`);
await queryRunner.query(`DROP TYPE "user_profile_includingnotificationtypes_enum"`);
}
}

View File

@@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class addSensitiveIndex1597385880794 implements MigrationInterface {
name = 'addSensitiveIndex1597385880794'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE INDEX "IDX_a7eba67f8b3fa27271e85d2e26" ON "drive_file" ("isSensitive") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_a7eba67f8b3fa27271e85d2e26"`);
}
}

View File

@@ -0,0 +1,27 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class channelUnread1597459042300 implements MigrationInterface {
name = 'channelUnread1597459042300'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`TRUNCATE TABLE "note_unread"`, undefined);
await queryRunner.query(`ALTER TABLE "channel_following" DROP COLUMN "readCursor"`);
await queryRunner.query(`ALTER TABLE "note_unread" ADD "isMentioned" boolean NOT NULL`);
await queryRunner.query(`ALTER TABLE "note_unread" ADD "noteChannelId" character varying(32)`);
await queryRunner.query(`CREATE INDEX "IDX_25b1dd384bec391b07b74b861c" ON "note_unread" ("isMentioned") `);
await queryRunner.query(`CREATE INDEX "IDX_89a29c9237b8c3b6b3cbb4cb30" ON "note_unread" ("isSpecified") `);
await queryRunner.query(`CREATE INDEX "IDX_29e8c1d579af54d4232939f994" ON "note_unread" ("noteUserId") `);
await queryRunner.query(`CREATE INDEX "IDX_6a57f051d82c6d4036c141e107" ON "note_unread" ("noteChannelId") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_6a57f051d82c6d4036c141e107"`);
await queryRunner.query(`DROP INDEX "IDX_29e8c1d579af54d4232939f994"`);
await queryRunner.query(`DROP INDEX "IDX_89a29c9237b8c3b6b3cbb4cb30"`);
await queryRunner.query(`DROP INDEX "IDX_25b1dd384bec391b07b74b861c"`);
await queryRunner.query(`ALTER TABLE "note_unread" DROP COLUMN "noteChannelId"`);
await queryRunner.query(`ALTER TABLE "note_unread" DROP COLUMN "isMentioned"`);
await queryRunner.query(`ALTER TABLE "channel_following" ADD "readCursor" TIMESTAMP WITH TIME ZONE NOT NULL`);
}
}

View File

@@ -0,0 +1,16 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class ChannelNoteIdDescIndex1597893996136 implements MigrationInterface {
name = 'ChannelNoteIdDescIndex1597893996136'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_f22169eb10657bded6d875ac8f"`);
await queryRunner.query(`CREATE INDEX "IDX_note_on_channelId_and_id_desc" ON "note" ("channelId", "id" desc)`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_note_on_channelId_and_id_desc"`);
await queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId") `);
}
}

View File

@@ -0,0 +1,20 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class mutingNotificationTypes1600353287890 implements MigrationInterface {
name = 'mutingNotificationTypes1600353287890'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "includingNotificationTypes"`);
await queryRunner.query(`DROP TYPE "public"."user_profile_includingnotificationtypes_enum"`);
await queryRunner.query(`CREATE TYPE "user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutingNotificationTypes" "user_profile_mutingnotificationtypes_enum" array NOT NULL DEFAULT '{}'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutingNotificationTypes"`);
await queryRunner.query(`DROP TYPE "user_profile_mutingnotificationtypes_enum"`);
await queryRunner.query(`CREATE TYPE "public"."user_profile_includingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "includingNotificationTypes" "user_profile_includingnotificationtypes_enum" array`);
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.45.1",
"version": "12.48.0",
"codename": "indigo",
"repository": {
"type": "git",
@@ -30,27 +30,26 @@
"resolutions": {
"chokidar": "^3.3.1",
"constantinople": "^4.0.1",
"core-js": "^3.6.5",
"gulp/gulp-cli/yargs/yargs-parser": "5.0.0-security.0",
"lodash": "^4.17.19",
"mocha/serialize-javascript": "^3.1.0"
"jsonld/rdf-canonize/node-forge": "0.10.0",
"lodash": "^4.17.20"
},
"dependencies": {
"@babel/plugin-transform-runtime": "7.11.0",
"@elastic/elasticsearch": "7.8.0",
"@fortawesome/fontawesome-svg-core": "1.2.30",
"@fortawesome/free-brands-svg-icons": "5.14.0",
"@fortawesome/free-regular-svg-icons": "5.14.0",
"@fortawesome/free-solid-svg-icons": "5.14.0",
"@fortawesome/vue-fontawesome": "0.1.10",
"@fortawesome/fontawesome-svg-core": "1.2.32",
"@fortawesome/free-brands-svg-icons": "5.15.1",
"@fortawesome/free-regular-svg-icons": "5.15.1",
"@fortawesome/free-solid-svg-icons": "5.15.1",
"@fortawesome/vue-fontawesome": "3.0.0-2",
"@koa/cors": "3.1.0",
"@koa/multer": "3.0.0",
"@koa/router": "9.0.1",
"@sinonjs/fake-timers": "6.0.1",
"@syuilo/aiscript": "0.10.1",
"@syuilo/aiscript": "0.11.0",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.14.0",
"@types/cbor": "5.0.0",
"@types/cbor": "5.0.1",
"@types/dateformat": "3.0.1",
"@types/double-ended-queue": "2.1.1",
"@types/escape-regexp": "0.0.0",
@@ -98,24 +97,25 @@
"@types/speakeasy": "2.0.5",
"@types/tinycolor2": "1.4.2",
"@types/tmp": "0.2.0",
"@types/uuid": "8.0.0",
"@types/uuid": "8.3.0",
"@types/web-push": "3.3.0",
"@types/webpack": "4.41.18",
"@types/webpack": "4.41.22",
"@types/webpack-stream": "3.2.11",
"@types/websocket": "1.0.1",
"@types/ws": "7.2.6",
"@typescript-eslint/parser": "3.6.0",
"@types/ws": "7.2.7",
"@typescript-eslint/parser": "4.4.0",
"@vue/compiler-sfc": "3.0.0",
"abort-controller": "3.0.0",
"apexcharts": "3.20.0",
"apexcharts": "3.22.0",
"autobind-decorator": "2.4.0",
"autosize": "4.0.2",
"autwh": "0.1.0",
"aws-sdk": "2.724.0",
"aws-sdk": "2.770.0",
"bcryptjs": "2.4.3",
"blurhash": "1.1.3",
"bull": "3.16.0",
"bull": "3.18.0",
"cafy": "15.2.1",
"cbor": "5.0.2",
"cbor": "5.1.0",
"chalk": "4.1.0",
"chart.js": "2.9.3",
"cli-highlight": "2.1.4",
@@ -123,42 +123,40 @@
"content-disposition": "0.5.3",
"core-js": "3.6.5",
"crc-32": "1.2.0",
"css-loader": "4.1.1",
"css-loader": "4.3.0",
"cssnano": "4.1.10",
"dateformat": "3.0.3",
"deep-entries": "3.1.0",
"diskusage": "1.1.3",
"double-ended-queue": "2.1.0-0",
"escape-regexp": "0.0.1",
"eslint": "7.4.0",
"eslint-plugin-vue": "6.2.2",
"eventemitter3": "4.0.4",
"eslint": "7.10.0",
"eslint-plugin-vue": "7.0.1",
"eventemitter3": "4.0.7",
"feed": "4.2.1",
"fibers": "5.0.0",
"file-type": "14.6.2",
"file-type": "15.0.1",
"fluent-ffmpeg": "2.1.2",
"glob": "7.1.6",
"gulp": "4.0.2",
"gulp-clean-css": "4.3.0",
"gulp-dart-sass": "1.0.2",
"gulp-rename": "2.0.0",
"gulp-replace": "1.0.0",
"gulp-sourcemaps": "2.6.5",
"gulp-terser": "1.2.1",
"gulp-terser": "1.4.0",
"gulp-tslint": "8.1.4",
"gulp-typescript": "6.0.0-alpha.1",
"hard-source-webpack-plugin": "0.13.1",
"hcaptcha": "0.0.2",
"html-minifier": "4.0.0",
"http-proxy-agent": "4.0.1",
"http-signature": "1.3.4",
"http-signature": "1.3.5",
"https-proxy-agent": "5.0.0",
"idb-keyval": "3.2.0",
"insert-text-at-cursor": "0.3.0",
"is-root": "2.1.0",
"is-svg": "4.2.1",
"js-yaml": "3.14.0",
"jsdom": "16.3.0",
"jsdom": "16.4.0",
"json5": "2.1.3",
"json5-loader": "4.0.0",
"jsonld": "3.1.1",
@@ -172,38 +170,38 @@
"koa-mount": "4.0.0",
"koa-send": "5.0.1",
"koa-slow": "2.1.0",
"koa-views": "6.3.0",
"koa-views": "6.3.1",
"langmap": "0.0.16",
"lookup-dns-cache": "2.1.0",
"markdown-it": "11.0.0",
"markdown-it-anchor": "5.3.0",
"mocha": "8.0.1",
"markdown-it": "11.0.1",
"markdown-it-anchor": "6.0.0",
"mocha": "8.1.3",
"moji": "0.5.1",
"ms": "2.1.2",
"multer": "1.4.2",
"nested-property": "2.0.1",
"node-fetch": "2.6.0",
"nodemailer": "6.4.10",
"nprogress": "0.2.0",
"nested-property": "4.0.0",
"node-fetch": "2.6.1",
"nodemailer": "6.4.13",
"object-assign-deep": "0.4.0",
"os-utils": "0.0.14",
"p-cancelable": "2.0.0",
"parse5": "6.0.1",
"parsimmon": "1.15.0",
"pg": "8.3.0",
"portal-vue": "2.1.7",
"parsimmon": "1.16.0",
"pg": "8.4.1",
"portscanner": "2.2.0",
"postcss-loader": "3.0.0",
"prismjs": "1.20.0",
"postcss": "8.1.1",
"postcss-loader": "4.0.3",
"prismjs": "1.21.0",
"probe-image-size": "5.0.0",
"promise-limit": "2.7.0",
"promise-sequential": "1.1.1",
"pug": "2.0.4",
"punycode": "2.1.1",
"pureimage": "0.2.1",
"pureimage": "0.2.5",
"qrcode": "1.4.4",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.15.4",
"re2": "1.15.5",
"recaptcha-promise": "0.1.3",
"reconnecting-websocket": "4.4.0",
"redis": "3.0.2",
@@ -216,54 +214,49 @@
"rimraf": "3.0.2",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sass": "1.26.10",
"sass-loader": "9.0.2",
"sass": "1.27.0",
"sass-loader": "10.0.2",
"seedrandom": "3.0.5",
"sharp": "0.25.4",
"sharp": "0.26.1",
"speakeasy": "2.0.0",
"stringz": "2.1.0",
"style-loader": "1.2.1",
"style-loader": "1.3.0",
"summaly": "2.4.0",
"syslog-pro": "1.0.0",
"systeminformation": "4.26.10",
"systeminformation": "4.27.8",
"syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0",
"three": "0.117.1",
"tinycolor2": "1.4.1",
"tinycolor2": "1.4.2",
"tmp": "0.2.1",
"ts-loader": "8.0.1",
"ts-node": "8.10.2",
"tslint": "6.1.2",
"ts-loader": "8.0.4",
"ts-node": "9.0.0",
"tslint": "6.1.3",
"tslint-sonarts": "1.9.0",
"typeorm": "0.2.25",
"typescript": "3.9.7",
"typeorm": "0.2.28",
"typescript": "4.0.3",
"ulid": "2.3.0",
"url-loader": "4.1.0",
"uuid": "8.3.0",
"v-animate-css": "0.0.3",
"uuid": "8.3.1",
"v-debounce": "0.1.2",
"vue": "2.6.11",
"vue": "3.0.1",
"vue-color": "2.7.1",
"vue-content-loading": "1.6.0",
"vue-cropperjs": "4.1.0",
"vue-i18n": "8.20.0",
"vue-json-pretty": "1.6.5",
"vue-loader": "15.9.3",
"vue-marquee-text-component": "1.1.1",
"vue-meta": "2.4.0",
"vue-draggable-next": "1.0.8",
"vue-i18n": "9.0.0-beta.4",
"vue-json-pretty": "1.7.0",
"vue-loader": "16.0.0-beta.7",
"vue-prism-component": "1.2.0",
"vue-prism-editor": "0.6.1",
"vue-router": "3.3.4",
"vue-prism-editor": "1.2.2",
"vue-router": "4.0.0-beta.13",
"vue-style-loader": "4.1.2",
"vue-svg-inline-loader-corejs3": "1.5.0",
"vue-template-compiler": "2.6.11",
"vuedraggable": "2.24.0",
"vuex": "3.5.1",
"vuex-persistedstate": "3.0.1",
"vue-template-compiler": "2.6.12",
"vuex": "4.0.0-beta.4",
"vuex-persistedstate": "3.1.0",
"web-push": "3.4.4",
"webpack": "5.0.0-beta.22",
"webpack": "5.1.3",
"webpack-cli": "3.3.12",
"websocket": "1.0.31",
"websocket": "1.0.32",
"ws": "7.3.1",
"xev": "2.0.1"
},

View File

@@ -1,21 +0,0 @@
type Obj = { [key: string]: any };
declare module 'nested-property' {
interface IHasNestedPropertyOptions {
own?: boolean;
}
interface IIsInNestedPropertyOptions {
validPath?: boolean;
}
export function set<T>(object: T, property: string, value: any): T;
export function get(object: Obj, property: string): any;
export function has(object: Obj, property: string, options?: IHasNestedPropertyOptions): boolean;
export function hasOwn(object: Obj, property: string, options?: IHasNestedPropertyOptions): boolean;
export function isIn(object: Obj, property: string, objectInPath: Obj, options?: IIsInNestedPropertyOptions): boolean;
}

12
src/client/.eslintrc Normal file
View File

@@ -0,0 +1,12 @@
{
"globals": {
"_DEV_": false,
"_LANGS_": false,
"_VERSION_": false,
"_ENV_": false,
"_PERF_PREFIX_": false,
"_DATA_TRANSFER_DRIVE_FILE_": false,
"_DATA_TRANSFER_DRIVE_FOLDER_": false,
"_DATA_TRANSFER_DECK_COLUMN_": false
}
}

8
src/client/@types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
declare const _LANGS_: string[];
declare const _VERSION_: string;
declare const _ENV_: string;
declare const _DEV_: boolean;
declare const _PERF_PREFIX_: string;
declare const _DATA_TRANSFER_DRIVE_FILE_: string;
declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
declare const _DATA_TRANSFER_DECK_COLUMN_: string;

11
src/client/@types/vuex-shim.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
import { ComponentCustomProperties } from 'vue';
import { Store } from 'vuex';
declare module '@vue/runtime-core' {
interface State {
}
interface ComponentCustomProperties {
$store: Store<State>
}
}

View File

@@ -1,773 +0,0 @@
<template>
<div class="mk-app" v-hotkey.global="keymap">
<header class="header">
<div class="title" ref="title">
<transition :name="$store.state.device.animation ? 'header' : ''" mode="out-in" appear>
<button class="_button back" v-if="canBack" @click="back()"><fa :icon="faChevronLeft"/></button>
</transition>
<transition :name="$store.state.device.animation ? 'header' : ''" mode="out-in" appear>
<div class="body" :key="pageKey">
<div class="default">
<portal-target name="avatar" slim/>
<h1 class="title"><portal-target name="icon" slim/><portal-target name="title" slim/></h1>
</div>
<div class="custom">
<portal-target name="header" slim/>
</div>
</div>
</transition>
</div>
<div class="sub">
<template v-if="$store.getters.isSignedIn">
<button v-if="widgetsEditMode" class="_button edit active" @click="widgetsEditMode = false"><fa :icon="faGripVertical"/></button>
<button v-else class="_button edit" @click="widgetsEditMode = true"><fa :icon="faGripVertical"/></button>
</template>
<div class="search">
<fa :icon="faSearch"/>
<input type="search" :placeholder="$t('search')" v-model="searchQuery" v-autocomplete="{ model: 'searchQuery' }" :disabled="searchWait" @keypress="searchKeypress"/>
</div>
<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
<x-clock v-if="isDesktop" class="clock"/>
</div>
</header>
<x-sidebar ref="nav"/>
<div class="contents" ref="contents" :class="{ wallpaper }">
<main ref="main">
<div class="content">
<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
<keep-alive :include="['index']">
<router-view></router-view>
</keep-alive>
</transition>
</div>
<div class="powerd-by" :class="{ visible: !$store.getters.isSignedIn }">
<b><router-link to="/">{{ host }}</router-link></b>
<small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small>
</div>
</main>
<template v-if="isDesktop">
<div v-for="place in ['left', 'right']" ref="widgets" class="widgets" :class="{ edit: widgetsEditMode, fixed: $store.state.device.fixedWidgetsPosition, empty: widgets[place].length === 0 && !widgetsEditMode }" :key="place">
<div class="spacer"></div>
<div class="container" v-if="widgetsEditMode">
<mk-button primary @click="addWidget(place)" class="add"><fa :icon="faPlus"/></mk-button>
<x-draggable
:list="widgets[place]"
handle=".handle"
animation="150"
class="sortable"
@sort="onWidgetSort"
>
<div v-for="widget in widgets[place]" class="customize-container _panel" :key="widget.id">
<header>
<span class="handle"><fa :icon="faBars"/></span>{{ $t('_widgets.' + widget.name) }}<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button>
</header>
<div @click="widgetFunc(widget.id)">
<component class="_close_ _forceContainerFull_" :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/>
</div>
</div>
</x-draggable>
</div>
<div class="container" v-else>
<component v-for="widget in widgets[place]" class="_close_ _forceContainerFull_" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget"/>
</div>
</div>
</template>
</div>
<div class="buttons">
<button class="button nav _button" @click="showNav" ref="navButton"><fa :icon="faBars"/><i v-if="navIndicated"><fa :icon="faCircle"/></i></button>
<button v-if="$route.name === 'index'" class="button home _button" @click="top()"><fa :icon="faHome"/></button>
<button v-else class="button home _button" @click="$router.push('/')"><fa :icon="faHome"/></button>
<button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="$router.push('/my/notifications')"><fa :icon="faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
<button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
</div>
<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
<stream-indicator v-if="$store.getters.isSignedIn"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faInfoCircle, faQuestionCircle, faProjectDiagram } from '@fortawesome/free-solid-svg-icons';
import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
import { v4 as uuid } from 'uuid';
import { host } from './config';
import { search } from './scripts/search';
import { StickySidebar } from './scripts/sticky-sidebar';
import { widgets } from './widgets';
import XSidebar from './components/sidebar.vue';
const DESKTOP_THRESHOLD = 1100;
export default Vue.extend({
components: {
XSidebar,
XClock: () => import('./components/header-clock.vue').then(m => m.default),
MkButton: () => import('./components/ui/button.vue').then(m => m.default),
XDraggable: () => import('vuedraggable'),
},
data() {
return {
host: host,
pageKey: 0,
searching: false,
connection: null,
searchQuery: '',
searchWait: false,
widgetsEditMode: false,
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
canBack: false,
menuDef: this.$store.getters.nav({}),
wallpaper: localStorage.getItem('wallpaper') != null,
faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram
};
},
computed: {
keymap(): any {
return {
'd': () => {
if (this.$store.state.device.syncDeviceDarkMode) return;
this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
},
'p': this.post,
'n': this.post,
's': this.search,
'h|/': this.help
};
},
widgets(): any {
if (this.$store.getters.isSignedIn) {
const widgets = this.$store.state.deviceUser.widgets;
return {
left: widgets.filter(x => x.place === 'left'),
right: widgets.filter(x => x.place == null || x.place === 'right'),
mobile: widgets.filter(x => x.place === 'mobile'),
};
} else {
const right = [{
name: 'calendar',
id: 'b', place: 'right', data: {}
}, {
name: 'trends',
id: 'c', place: 'right', data: {}
}];
if (this.$route.name !== 'index') {
right.unshift({
name: 'welcome',
id: 'a', place: 'right', data: {}
});
}
return {
left: [],
right,
mobile: [],
};
}
},
menu(): string[] {
return this.$store.state.deviceUser.menu;
},
navIndicated(): boolean {
if (!this.$store.getters.isSignedIn) return false;
for (const def in this.menuDef) {
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
if (this.menuDef[def].indicated) return true;
}
return false;
}
},
watch: {
$route(to, from) {
this.pageKey++;
this.canBack = (window.history.length > 0 && !['index'].includes(to.name));
},
isDesktop() {
this.$nextTick(() => {
this.attachSticky();
});
}
},
created() {
document.documentElement.style.overflowY = 'scroll';
if (this.$store.getters.isSignedIn) {
this.connection = this.$root.stream.useSharedConnection('main');
this.connection.on('notification', this.onNotification);
if (this.$store.state.deviceUser.widgets.length === 0) {
this.$store.commit('deviceUser/setWidgets', [{
name: 'calendar',
id: 'a', place: 'right', data: {}
}, {
name: 'notifications',
id: 'b', place: 'right', data: {}
}, {
name: 'trends',
id: 'c', place: 'right', data: {}
}]);
}
}
},
mounted() {
const adjustTitlePosition = () => {
const left = this.$refs.main.getBoundingClientRect().left - this.$refs.nav.$el.offsetWidth;
if (left >= 0) {
this.$refs.title.style.left = left + 'px';
}
};
adjustTitlePosition();
const ro = new ResizeObserver((entries, observer) => {
adjustTitlePosition();
});
ro.observe(this.$refs.contents);
window.addEventListener('resize', adjustTitlePosition, { passive: true });
if (!this.isDesktop) {
window.addEventListener('resize', () => {
if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
}, { passive: true });
}
// widget follow
this.attachSticky();
},
methods: {
showNav() {
this.$refs.nav.show();
},
attachSticky() {
if (!this.isDesktop) return;
if (this.$store.state.device.fixedWidgetsPosition) return;
const stickyWidgetColumns = this.$refs.widgets.map(w => new StickySidebar(w.children[1], w.children[0], w.offsetTop));
window.addEventListener('scroll', () => {
for (const stickyWidgetColumn of stickyWidgetColumns) {
stickyWidgetColumn.calc(window.scrollY);
}
}, { passive: true });
},
top() {
window.scroll({ top: 0, behavior: 'smooth' });
},
help() {
this.$router.push('/docs/keyboard-shortcut');
},
back() {
if (this.canBack) window.history.back();
},
onTransition() {
if (window._scroll) window._scroll();
},
post() {
this.$root.post();
},
search() {
if (this.searching) return;
this.$root.dialog({
title: this.$t('search'),
input: true
}).then(async ({ canceled, result: query }) => {
if (canceled || query == null || query === '') return;
this.searching = true;
search(this, query).finally(() => {
this.searching = false;
});
});
},
searchKeypress(e) {
if (e.keyCode === 13) {
this.searchWait = true;
search(this, this.searchQuery).finally(() => {
this.searchWait = false;
this.searchQuery = '';
});
}
},
async onNotification(notification) {
if (document.visibilityState === 'visible') {
this.$root.stream.send('readNotification', {
id: notification.id
});
this.$root.new(await import('./components/toast.vue').then(m => m.default), {
notification
});
}
this.$root.sound('notification');
},
widgetFunc(id) {
this.$refs[id][0].setting();
},
onWidgetSort() {
this.saveHome();
},
async addWidget(place) {
const { canceled, result: widget } = await this.$root.dialog({
type: null,
title: this.$t('chooseWidget'),
select: {
items: widgets.map(widget => ({
value: widget,
text: this.$t('_widgets.' + widget),
}))
},
showCancelButton: true
});
if (canceled) return;
this.$store.commit('deviceUser/addWidget', {
name: widget,
id: uuid(),
place: place,
data: {}
});
},
removeWidget(widget) {
this.$store.commit('deviceUser/removeWidget', widget);
},
saveHome() {
this.$store.commit('deviceUser/setWidgets', [...this.widgets.left, ...this.widgets.right, ...this.widgets.mobile]);
}
}
});
</script>
<style lang="scss" scoped>
.mk-app {
$header-height: 60px;
$nav-width: 250px; // TODO: どこかに集約したい
$nav-icon-only-width: 80px; // TODO: どこかに集約したい
$main-width: 670px;
$ui-font-size: 1em; // TODO: どこかに集約したい
$nav-icon-only-threshold: 1279px; // TODO: どこかに集約したい
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
$header-sub-hide-threshold: 1090px;
$left-widgets-hide-threshold: 1600px;
$right-widgets-hide-threshold: 1090px;
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
min-height: calc(var(--vh, 1vh) * 100);
box-sizing: border-box;
padding-top: $header-height;
&, > .header > .body {
display: flex;
margin: 0 auto;
}
> .header {
position: fixed;
z-index: 1000;
top: 0;
right: 0;
height: $header-height;
width: calc(100% - #{$nav-width});
//background-color: var(--panel);
-webkit-backdrop-filter: blur(32px);
backdrop-filter: blur(32px);
background-color: var(--header);
border-bottom: solid 1px var(--divider);
@media (max-width: $nav-icon-only-threshold) {
width: calc(100% - #{$nav-icon-only-width});
}
@media (max-width: $nav-hide-threshold) {
width: 100%;
}
> .title {
position: relative;
line-height: $header-height;
height: $header-height;
max-width: $main-width;
text-align: center;
> .back {
position: absolute;
z-index: 1;
top: 0;
left: 0;
height: $header-height;
width: $header-height;
}
> .body {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: $header-height;
> .default {
padding: 0 $header-height;
> .avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: (($header-height - $size) / 2) 8px (($header-height - $size) / 2) 0;
}
> .title {
display: inline-block;
font-size: $ui-font-size;
margin: 0;
line-height: $header-height;
> [data-icon] {
margin-right: 8px;
}
}
}
> .custom {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
}
}
> .sub {
$post-button-size: 42px;
$post-button-margin: (($header-height - $post-button-size) / 2);
display: flex;
align-items: center;
position: absolute;
top: 0;
right: 16px;
height: $header-height;
@media (max-width: $header-sub-hide-threshold) {
display: none;
}
> .edit {
padding: 16px;
&.active {
color: var(--accent);
}
}
> .search {
position: relative;
> input {
width: 220px;
box-sizing: border-box;
margin-right: 8px;
padding: 0 12px 0 42px;
font-size: 1rem;
line-height: 38px;
border: none;
border-radius: 38px;
color: var(--fg);
background: var(--bg);
-webkit-appearance: textfield;
&:focus {
outline: none;
}
}
> [data-icon] {
position: absolute;
top: 0;
left: 16px;
height: 100%;
pointer-events: none;
font-size: 16px;
}
}
> .post {
width: $post-button-size;
height: $post-button-size;
margin-left: $post-button-margin;
border-radius: 100%;
font-size: 16px;
}
> .clock {
margin-left: 8px;
}
}
}
> .contents {
display: flex;
margin: 0 auto;
min-width: 0;
&.wallpaper {
background: var(--wallpaperOverlay);
backdrop-filter: blur(4px);
}
> main {
width: $main-width;
min-width: 0;
> .content {
> * {
// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
min-height: calc((var(--vh, 1vh) * 100) - #{$header-height});
box-sizing: border-box;
padding: var(--margin);
&.full {
padding: 0 var(--margin);
}
}
}
> .powerd-by {
font-size: 14px;
text-align: center;
margin: 32px 0;
visibility: hidden;
&.visible {
visibility: visible;
}
&:not(.visible) {
@media (min-width: 850px) {
display: none;
}
}
@media (max-width: 500px) {
margin-top: 16px;
}
> small {
display: block;
margin-top: 8px;
opacity: 0.5;
@media (max-width: 500px) {
margin-top: 4px;
}
}
}
}
> .widgets {
padding: 0 var(--margin);
box-shadow: 1px 0 0 0 var(--divider), -1px 0 0 0 var(--divider);
&.fixed {
position: sticky;
overflow: auto;
// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
height: calc((var(--vh, 1vh) * 100) - #{$header-height});
top: $header-height;
}
&:first-of-type {
order: -1;
@media (max-width: $left-widgets-hide-threshold) {
display: none;
}
}
&.empty {
display: none;
}
@media (max-width: $right-widgets-hide-threshold) {
display: none;
}
> .container {
position: sticky;
height: min-content;
// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
min-height: calc((var(--vh, 1vh) * 100) - #{$header-height});
padding: var(--margin) 0;
box-sizing: border-box;
> * {
margin: var(--margin) 0;
width: 300px;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
}
> .add {
margin: 0 auto;
}
.customize-container {
margin: 8px 0;
> header {
position: relative;
line-height: 32px;
> .handle {
padding: 0 8px;
cursor: move;
}
> .remove {
position: absolute;
top: 0;
right: 0;
padding: 0 8px;
line-height: 32px;
}
}
> div {
padding: 8px;
> * {
pointer-events: none;
}
}
}
}
}
> .post {
display: none;
position: fixed;
z-index: 1000;
bottom: 32px;
right: 32px;
width: 64px;
height: 64px;
border-radius: 100%;
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
font-size: 22px;
@media (min-width: ($nav-hide-threshold + 1px)) {
display: block;
}
@media (min-width: ($header-sub-hide-threshold + 1px)) {
display: none;
}
}
> .buttons {
position: fixed;
z-index: 1000;
bottom: 0;
padding: 0 32px 32px 32px;
display: flex;
width: 100%;
box-sizing: border-box;
background: linear-gradient(0deg, var(--bg), var(--X1));
@media (max-width: 500px) {
padding: 0 16px 16px 16px;
}
@media (min-width: ($nav-hide-threshold + 1px)) {
display: none;
}
> .button {
position: relative;
padding: 0;
margin: auto;
width: 64px;
height: 64px;
border-radius: 100%;
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
> * {
font-size: 22px;
}
&:disabled {
cursor: default;
> * {
opacity: 0.5;
}
}
&:not(.post) {
background: var(--panel);
color: var(--fg);
&:hover {
background: var(--X2);
}
> i {
position: absolute;
top: 0;
left: 0;
color: var(--indicator);
font-size: 16px;
animation: blink 1s infinite;
}
}
}
}
}
</style>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -6,11 +6,11 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { toUnicode } from 'punycode';
import { host } from '../config';
import { host } from '@/config';
export default Vue.extend({
export default defineComponent({
props: ['user', 'detail'],
data() {
return {

View File

@@ -34,10 +34,11 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import * as tinycolor from 'tinycolor2';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
data() {
return {
now: new Date(),
@@ -127,7 +128,7 @@ export default Vue.extend({
});
},
beforeDestroy() {
beforeUnmount() {
this.enabled = false;
},

View File

@@ -1,12 +1,12 @@
<template>
<div class="swhvrteh" @contextmenu.prevent="() => {}">
<div class="swhvrteh _popup _shadow" @contextmenu.prevent="() => {}">
<ol class="users" ref="suggests" v-if="type === 'user'">
<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1" class="user">
<img class="avatar" :src="user.avatarUrl"/>
<span class="name">
<mk-user-name :user="user" :key="user.id"/>
<MkUserName :user="user" :key="user.id"/>
</span>
<span class="username">@{{ user | acct }}</span>
<span class="username">@{{ acct(user) }}</span>
</li>
<li @click="chooseUser()" @keydown="onKeydown" tabindex="-1" class="choose">{{ $t('selectUser') }}</li>
</ol>
@@ -28,12 +28,13 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { emojilist } from '../../misc/emojilist';
import contains from '../scripts/contains';
import contains from '@/scripts/contains';
import { twemojiSvgBase } from '../../misc/twemoji-base';
import { getStaticImageUrl } from '../scripts/get-static-image-url';
import MkUserSelect from './user-select.vue';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { acct } from '@/filters/user';
import * as os from '@/os';
type EmojiDef = {
emoji: string;
@@ -74,7 +75,7 @@ for (const x of lib) {
emjdb.sort((a, b) => a.name.length - b.name.length);
export default Vue.extend({
export default defineComponent({
props: {
type: {
type: String,
@@ -91,11 +92,6 @@ export default Vue.extend({
required: true,
},
complete: {
type: Function,
required: true,
},
close: {
type: Function,
required: true,
@@ -110,8 +106,15 @@ export default Vue.extend({
type: Number,
required: true,
},
showing: {
type: Boolean,
required: true
},
},
emits: ['done', 'closed'],
data() {
return {
getStaticImageUrl,
@@ -135,6 +138,14 @@ export default Vue.extend({
}
},
watch: {
showing() {
if (!this.showing) {
this.$emit('closed');
}
}
},
updated() {
this.setPosition();
},
@@ -189,7 +200,7 @@ export default Vue.extend({
});
},
beforeDestroy() {
beforeUnmount() {
this.textarea.removeEventListener('keydown', this.onKeydown);
for (const el of Array.from(document.querySelectorAll('body *'))) {
@@ -198,6 +209,11 @@ export default Vue.extend({
},
methods: {
complete(type, value) {
this.$emit('done', { type, value });
this.$emit('closed');
},
setPosition() {
if (this.x + this.$el.offsetWidth > window.innerWidth) {
this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px';
@@ -236,8 +252,8 @@ export default Vue.extend({
this.users = users;
this.fetching = false;
} else {
this.$root.api('users/search', {
query: this.q,
os.api('users/search-by-username-and-host', {
username: this.q,
limit: 10,
detail: false
}).then(users => {
@@ -260,7 +276,7 @@ export default Vue.extend({
this.hashtags = hashtags;
this.fetching = false;
} else {
this.$root.api('hashtags/search', {
os.api('hashtags/search', {
query: this.q,
limit: 30
}).then(hashtags => {
@@ -374,14 +390,13 @@ export default Vue.extend({
chooseUser() {
this.close();
const vm = this.$root.new(MkUserSelect, {});
vm.$once('selected', user => {
os.selectUser().then(user => {
this.complete('user', user);
});
vm.$once('closed', () => {
this.textarea.focus();
});
}
},
acct
}
});
</script>
@@ -393,9 +408,6 @@ export default Vue.extend({
max-width: 100%;
margin-top: calc(1em + 8px);
overflow: hidden;
background: var(--panel);
border: solid 1px rgba(#000, 0.1);
border-radius: 4px;
transition: top 0.1s ease, left 0.1s ease;
> ol {

View File

@@ -1,17 +1,19 @@
<template>
<span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick">
<span class="eiwwqkts" :class="{ cat }" :title="acct(user)" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick">
<img class="inner" :src="url"/>
</span>
<router-link class="eiwwqkts" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
<router-link class="eiwwqkts" :class="{ cat }" :to="userPage(user)" :title="acct(user)" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
<img class="inner" :src="url"/>
</router-link>
</template>
<script lang="ts">
import Vue from 'vue';
import { getStaticImageUrl } from '../scripts/get-static-image-url';
import { defineComponent } from 'vue';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
import { acct, userPage } from '../filters/user';
export default Vue.extend({
export default defineComponent({
props: {
user: {
type: Object,
@@ -30,6 +32,7 @@ export default Vue.extend({
default: false
}
},
emits: ['click'],
computed: {
cat(): boolean {
return this.user.isCat;
@@ -42,25 +45,19 @@ export default Vue.extend({
},
watch: {
'user.avatarBlurhash'() {
this.$el.style.color = this.getBlurhashAvgColor(this.user.avatarBlurhash);
if (this.$el == null) return;
this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
}
},
mounted() {
this.$el.style.color = this.getBlurhashAvgColor(this.user.avatarBlurhash);
this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
},
methods: {
getBlurhashAvgColor(s) {
return typeof s == 'string'
? '#' + [...s.slice(2, 6)]
.map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x))
.reduce((a, c) => a * 83 + c, 0)
.toString(16)
.padStart(6, '0')
: undefined;
},
onClick(e) {
this.$emit('click', e);
}
},
acct,
userPage
}
});
</script>
@@ -95,7 +92,7 @@ export default Vue.extend({
transform: rotate(-37.5deg) skew(-30deg);
}
}
.inner {
position: absolute;
bottom: 0;

View File

@@ -1,15 +1,16 @@
<template>
<div>
<div v-for="user in us" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
<mk-avatar :user="user" style="width:32px;height:32px;"/>
<MkAvatar :user="user" style="width:32px;height:32px;"/>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
userIds: {
required: true
@@ -21,7 +22,7 @@ export default Vue.extend({
};
},
async created() {
this.us = await this.$root.api('users/show', {
this.us = await os.api('users/show', {
userIds: this.userIds
});
}

View File

@@ -1,12 +1,12 @@
<template>
<div>
<span v-if="!available">{{ $t('waiting') }}<mk-ellipsis/></span>
<span v-if="!available">{{ $t('waiting') }}<MkEllipsis/></span>
<div ref="captcha"></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
type Captcha = {
render(container: string | Node, options: {
@@ -28,8 +28,9 @@ declare global {
interface Window extends CaptchaContainer {
}
}
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
provider: {
type: String,
@@ -88,7 +89,7 @@ export default Vue.extend({
}
},
beforeDestroy() {
beforeUnmount() {
this.reset();
},
@@ -110,7 +111,7 @@ export default Vue.extend({
}
},
callback(response?: string) {
this.$emit('input', typeof response == 'string' ? response : null);
this.$emit('update:value', typeof response == 'string' ? response : null);
},
},
});

View File

@@ -0,0 +1,142 @@
<template>
<button class="hdcaacmi _button"
:class="{ wait, active: isFollowing, full }"
@click="onClick"
:disabled="wait"
>
<template v-if="!wait">
<template v-if="isFollowing">
<span v-if="full">{{ $t('unfollow') }}</span><Fa :icon="faMinus"/>
</template>
<template v-else>
<span v-if="full">{{ $t('follow') }}</span><Fa :icon="faPlus"/>
</template>
</template>
<template v-else>
<span v-if="full">{{ $t('processing') }}</span><Fa :icon="faSpinner" pulse fixed-width/>
</template>
</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faSpinner, faPlus, faMinus, } from '@fortawesome/free-solid-svg-icons';
import * as os from '@/os';
export default defineComponent({
props: {
channel: {
type: Object,
required: true
},
full: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isFollowing: this.channel.isFollowing,
wait: false,
faSpinner, faPlus, faMinus,
};
},
methods: {
async onClick() {
this.wait = true;
try {
if (this.isFollowing) {
await os.api('channels/unfollow', {
channelId: this.channel.id
});
this.isFollowing = false;
} else {
await os.api('channels/follow', {
channelId: this.channel.id
});
this.isFollowing = true;
}
} catch (e) {
console.error(e);
} finally {
this.wait = false;
}
}
}
});
</script>
<style lang="scss" scoped>
.hdcaacmi {
position: relative;
display: inline-block;
font-weight: bold;
color: var(--accent);
background: transparent;
border: solid 1px var(--accent);
padding: 0;
height: 31px;
font-size: 16px;
border-radius: 32px;
background: #fff;
&.full {
padding: 0 8px 0 12px;
font-size: 14px;
}
&:not(.full) {
width: 31px;
}
&:focus {
&:after {
content: "";
pointer-events: none;
position: absolute;
top: -5px;
right: -5px;
bottom: -5px;
left: -5px;
border: 2px solid var(--focus);
border-radius: 32px;
}
}
&:hover {
//background: mix($primary, #fff, 20);
}
&:active {
//background: mix($primary, #fff, 40);
}
&.active {
color: #fff;
background: var(--accent);
&:hover {
background: var(--accentLighten);
border-color: var(--accentLighten);
}
&:active {
background: var(--accentDarken);
border-color: var(--accentDarken);
}
}
&.wait {
cursor: wait !important;
opacity: 0.7;
}
> span {
margin-right: 6px;
}
}
</style>

View File

@@ -0,0 +1,157 @@
<template>
<router-link :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
<div class="banner" v-if="channel.bannerUrl" :style="`background-image: url('${channel.bannerUrl}')`">
<div class="fade"></div>
<div class="name"><Fa :icon="faSatelliteDish"/> {{ channel.name }}</div>
<div class="status">
<div>
<Fa :icon="faUsers" fixed-width/>
<i18n-t keypath="_channel.usersCount" tag="span" style="margin-left: 4px;">
<template #n>
<b>{{ channel.usersCount }}</b>
</template>
</i18n-t>
</div>
<div>
<Fa :icon="faPencilAlt" fixed-width/>
<i18n-t keypath="_channel.notesCount" tag="span" style="margin-left: 4px;">
<template #n>
<b>{{ channel.notesCount }}</b>
</template>
</i18n-t>
</div>
</div>
</div>
<article v-if="channel.description">
<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
</article>
<footer>
<span v-if="channel.lastNotedAt">
{{ $t('updatedAt') }}: <MkTime :time="channel.lastNotedAt"/>
</span>
</footer>
</router-link>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faSatelliteDish, faUsers, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
export default defineComponent({
props: {
channel: {
type: Object,
required: true
},
},
data() {
return {
faSatelliteDish, faUsers, faPencilAlt,
};
},
});
</script>
<style lang="scss" scoped>
.eftoefju {
display: block;
overflow: hidden;
width: 100%;
&:hover {
text-decoration: none;
}
> .banner {
position: relative;
width: 100%;
height: 200px;
background-position: center;
background-size: cover;
> .fade {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
}
> .name {
position: absolute;
top: 16px;
left: 16px;
padding: 12px 16px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 1.2em;
}
> .status {
position: absolute;
z-index: 1;
bottom: 16px;
right: 16px;
padding: 8px 12px;
font-size: 80%;
background: rgba(0, 0, 0, 0.7);
border-radius: 6px;
color: #fff;
}
}
> article {
padding: 16px;
> p {
margin: 0;
font-size: 1em;
}
}
> footer {
padding: 12px 16px;
border-top: solid 1px var(--divider);
> span {
opacity: 0.7;
font-size: 0.9em;
}
}
@media (max-width: 550px) {
font-size: 0.9em;
> .banner {
height: 80px;
> .status {
display: none;
}
}
> article {
padding: 12px;
}
> footer {
display: none;
}
}
@media (max-width: 500px) {
font-size: 0.8em;
> .banner {
height: 70px;
}
> article {
padding: 8px;
}
}
}
</style>

View File

@@ -1,13 +1,14 @@
<template>
<x-prism :inline="inline" :language="prismLang">{{ code }}</x-prism>
<XPrism :inline="inline" :language="prismLang">{{ code }}</XPrism>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import 'prismjs';
import 'prismjs/themes/prism-okaidia.css';
import XPrism from 'vue-prism-component';
export default Vue.extend({
import XPrism from 'vue-prism-component';import * as os from '@/os';
export default defineComponent({
components: {
XPrism
},

View File

@@ -1,12 +1,13 @@
<template>
<x-code :code="code" :lang="lang" :inline="inline"/>
<XCode :code="code" :lang="lang" :inline="inline"/>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
import { defineComponent, defineAsyncComponent } from 'vue';
export default defineComponent({
components: {
XCode: () => import('./code-core.vue').then(m => m.default)
XCode: defineAsyncComponent(() => import('./code-core.vue'))
},
props: {
code: {

View File

@@ -1,16 +1,16 @@
<template>
<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh _button" @click="toggle">
<b>{{ value ? this.$t('_cw.hide') : this.$t('_cw.show') }}</b>
<span v-if="!value">{{ this.label }}</span>
<button class="nrvgflfu _button" @click="toggle">
<b>{{ value ? $t('_cw.hide') : $t('_cw.show') }}</b>
<span v-if="!value">{{ label }}</span>
</button>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { length } from 'stringz';
import { concat } from '../../prelude/array';
export default Vue.extend({
export default defineComponent({
props: {
value: {
type: Boolean,
@@ -36,14 +36,14 @@ export default Vue.extend({
length,
toggle() {
this.$emit('input', !this.value);
this.$emit('update:value', !this.value);
}
}
});
</script>
<style lang="scss" scoped>
.nrvgflfuaxwgkxoynpnumyookecqrrvh {
.nrvgflfu {
display: inline-block;
padding: 4px 8px;
font-size: 0.7em;

View File

@@ -1,22 +1,22 @@
<template>
<component :is="$store.state.device.animation ? 'transition-group' : 'div'" class="sqadhkmv _list_" name="list" tag="div" :data-direction="direction" :data-reversed="reversed ? 'true' : 'false'">
<transition-group class="sqadhkmv _list_" name="list" tag="div" :data-direction="direction" :data-reversed="reversed ? 'true' : 'false'">
<template v-for="(item, i) in items">
<slot :item="item"></slot>
<div class="separator" v-if="showDate(i, item)" :key="item.id + '_date'">
<p class="date">
<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span>
<span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span>
<span><Fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span>
<span>{{ getDateText(items[i + 1].createdAt) }}<Fa class="icon" :icon="faAngleDown"/></span>
</p>
</div>
</template>
</component>
</transition-group>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
export default defineComponent({
props: {
items: {
type: Array,
@@ -82,14 +82,14 @@ export default Vue.extend({
}
&[data-direction="up"] {
> .list-enter {
> .list-enter-from {
opacity: 0;
transform: translateY(64px);
}
}
&[data-direction="down"] {
> .list-enter {
> .list-enter-from {
opacity: 0;
transform: translateY(-64px);
}

View File

@@ -1,20 +1,21 @@
<template>
<x-column :menu="menu" :column="column" :is-stacked="isStacked">
<XColumn :menu="menu" :column="column" :is-stacked="isStacked">
<template #header>
<fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span>
<Fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span>
</template>
<x-timeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
</x-column>
<XTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
</XColumn>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faSatellite, faCog } from '@fortawesome/free-solid-svg-icons';
import XColumn from './column.vue';
import XTimeline from '../timeline.vue';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
XColumn,
XTimeline,
@@ -59,8 +60,8 @@ export default Vue.extend({
methods: {
async setAntenna() {
const antennas = await this.$root.api('antennas/list');
const { canceled, result: antenna } = await this.$root.dialog({
const antennas = await os.api('antennas/list');
const { canceled, result: antenna } = await os.dialog({
title: this.$t('selectAntenna'),
type: null,
select: {
@@ -72,7 +73,7 @@ export default Vue.extend({
showCancelButton: true
});
if (canceled) return;
Vue.set(this.column, 'antennaId', antenna.id);
this.column.antennaId = antenna.id;
this.$store.commit('deviceUser/updateDeckColumn', this.column);
},

View File

@@ -1,17 +1,17 @@
<template>
<!-- TODO: リファクタの余地がありそう -->
<x-widgets-column v-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<x-notifications-column v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<x-tl-column v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<x-list-column v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<x-antenna-column v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<!-- TODO: <x-tl-column v-else-if="column.type === 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -->
<x-mentions-column v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<x-direct-column v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<XWidgetsColumn v-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<!-- TODO: <XTlColumn v-else-if="column.type === 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -->
<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import XTlColumn from './tl-column.vue';
import XAntennaColumn from './antenna-column.vue';
import XListColumn from './list-column.vue';
@@ -20,7 +20,7 @@ import XWidgetsColumn from './widgets-column.vue';
import XMentionsColumn from './mentions-column.vue';
import XDirectColumn from './direct-column.vue';
export default Vue.extend({
export default defineComponent({
components: {
XTlColumn,
XAntennaColumn,

View File

@@ -1,6 +1,6 @@
<template>
<!-- sectionを利用しているのはdeck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
<section class="dnpfarvg _panel _narrow_" :class="{ naked, paged: isMainColumn, _close_: !isMainColumn, active, isStacked, draghover, dragging, dropready }"
<section class="dnpfarvg _panel _narrow_" :class="{ paged: isMainColumn, naked, _close_: !isMainColumn, active, isStacked, draghover, dragging, dropready }"
@dragover.prevent.stop="onDragover"
@dragleave="onDragleave"
@drop.prevent.stop="onDrop"
@@ -15,15 +15,14 @@
@contextmenu.prevent.stop="onContextmenu"
>
<button class="toggleActive _button" @click="toggleActive" v-if="isStacked">
<template v-if="active"><fa :icon="faAngleUp"/></template>
<template v-else><fa :icon="faAngleDown"/></template>
<template v-if="active"><Fa :icon="faAngleUp"/></template>
<template v-else><Fa :icon="faAngleDown"/></template>
</button>
<div class="action">
<slot name="action"></slot>
</div>
<span class="header"><slot name="header"></slot></span>
<button v-if="!isMainColumn" class="menu _button" ref="menu" @click.stop="showMenu"><fa :icon="faCaretDown"/></button>
<button v-else-if="$route.name !== 'index'" class="close _button" @click.stop="close"><fa :icon="faTimes"/></button>
<button v-if="!isMainColumn" class="menu _button" ref="menu" @click.stop="showMenu"><Fa :icon="faCaretDown"/></button>
</header>
<div ref="body" v-show="active">
<slot></slot>
@@ -32,11 +31,12 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes, faArrowRight, faArrowLeft, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { defineComponent } from 'vue';
import { faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faArrowRight, faArrowLeft, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { faWindowMaximize, faTrashAlt, faWindowRestore } from '@fortawesome/free-regular-svg-icons';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
column: {
type: Object,
@@ -71,7 +71,7 @@ export default Vue.extend({
dragging: false,
draghover: false,
dropready: false,
faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes,
faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown,
};
},
@@ -86,10 +86,10 @@ export default Vue.extend({
keymap(): any {
return {
'shift+up': () => this.$parent.$emit('parentFocus', 'up'),
'shift+down': () => this.$parent.$emit('parentFocus', 'down'),
'shift+left': () => this.$parent.$emit('parentFocus', 'left'),
'shift+right': () => this.$parent.$emit('parentFocus', 'right'),
'shift+up': () => this.$parent.$emit('parent-focus', 'up'),
'shift+down': () => this.$parent.$emit('parent-focus', 'down'),
'shift+left': () => this.$parent.$emit('parent-focus', 'left'),
'shift+right': () => this.$parent.$emit('parent-focus', 'right'),
};
}
},
@@ -100,21 +100,21 @@ export default Vue.extend({
},
dragging(v) {
this.$root.$emit(v ? 'deck.column.dragStart' : 'deck.column.dragEnd');
os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd');
}
},
mounted() {
if (!this.isMainColumn) {
this.$root.$on('deck.column.dragStart', this.onOtherDragStart);
this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd);
os.deckGlobalEvents.on('column.dragStart', this.onOtherDragStart);
os.deckGlobalEvents.on('column.dragEnd', this.onOtherDragEnd);
}
},
beforeDestroy() {
beforeUnmount() {
if (!this.isMainColumn) {
this.$root.$off('deck.column.dragStart', this.onOtherDragStart);
this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd);
os.deckGlobalEvents.off('column.dragStart', this.onOtherDragStart);
os.deckGlobalEvents.off('column.dragEnd', this.onOtherDragEnd);
}
},
@@ -137,7 +137,7 @@ export default Vue.extend({
icon: faPencilAlt,
text: this.$t('rename'),
action: () => {
this.$root.dialog({
os.dialog({
title: this.$t('rename'),
input: {
default: this.column.name,
@@ -207,14 +207,7 @@ export default Vue.extend({
},
showMenu() {
this.$root.menu({
items: this.getMenu(),
source: this.$refs.menu,
});
},
close() {
this.$router.push('/');
os.modalMenu(this.getMenu(), this.$refs.menu);
},
goTop() {
@@ -232,7 +225,7 @@ export default Vue.extend({
}
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('mk-deck-column', this.column.id);
e.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, this.column.id);
this.dragging = true;
},
@@ -254,7 +247,7 @@ export default Vue.extend({
return;
}
const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column';
const isDeckColumn = e.dataTransfer.types[0] == _DATA_TRANSFER_DECK_COLUMN_;
e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
@@ -267,9 +260,9 @@ export default Vue.extend({
onDrop(e) {
this.draghover = false;
this.$root.$emit('deck.column.dragEnd');
os.deckGlobalEvents.emit('column.dragEnd');
const id = e.dataTransfer.getData('mk-deck-column');
const id = e.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
if (id != null && id != '') {
this.$store.commit('deviceUser/swapDeckColumn', {
a: this.column.id,
@@ -285,9 +278,11 @@ export default Vue.extend({
.dnpfarvg {
$header-height: 42px;
--section-padding: 10px;
height: 100%;
overflow: hidden;
box-shadow: 0 0 0 1px var(--deckColumnBorder);
contain: content;
&.draghover {
box-shadow: 0 0 0 2px var(--focus);
@@ -341,7 +336,6 @@ export default Vue.extend({
&.paged {
> div {
background: var(--bg);
padding: var(--margin);
}
}
@@ -379,8 +373,7 @@ export default Vue.extend({
> .toggleActive,
> .action > *,
> .menu,
> .close {
> .menu {
z-index: 1;
width: $header-height;
line-height: $header-height;
@@ -408,8 +401,7 @@ export default Vue.extend({
display: none;
}
> .menu,
> .close {
> .menu {
margin-left: auto;
margin-right: -16px;
}

View File

@@ -1,19 +1,20 @@
<template>
<x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu">
<template #header><fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template>
<XColumn :name="name" :column="column" :is-stacked="isStacked" :menu="menu">
<template #header><Fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template>
<x-notes :pagination="pagination" @before="before()" @after="after()"/>
</x-column>
<XNotes :pagination="pagination" @before="before()" @after="after()"/>
</XColumn>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
import Progress from '../../scripts/loading';
import Progress from '@/scripts/loading';
import XColumn from './column.vue';
import XNotes from '../notes.vue';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
XColumn,
XNotes

View File

@@ -1,20 +1,21 @@
<template>
<x-column :menu="menu" :column="column" :is-stacked="isStacked">
<XColumn :menu="menu" :column="column" :is-stacked="isStacked">
<template #header>
<fa :icon="faListUl"/><span style="margin-left: 8px;">{{ column.name }}</span>
<Fa :icon="faListUl"/><span style="margin-left: 8px;">{{ column.name }}</span>
</template>
<x-timeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/>
</x-column>
<XTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/>
</XColumn>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faListUl, faCog } from '@fortawesome/free-solid-svg-icons';
import XColumn from './column.vue';
import XTimeline from '../timeline.vue';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
XColumn,
XTimeline,
@@ -59,8 +60,8 @@ export default Vue.extend({
methods: {
async setList() {
const lists = await this.$root.api('users/lists/list');
const { canceled, result: list } = await this.$root.dialog({
const lists = await os.api('users/lists/list');
const { canceled, result: list } = await os.dialog({
title: this.$t('selectList'),
type: null,
select: {
@@ -72,7 +73,7 @@ export default Vue.extend({
showCancelButton: true
});
if (canceled) return;
Vue.set(this.column, 'listId', list.id);
this.column.listId = list.id;
this.$store.commit('deviceUser/updateDeckColumn', this.column);
},

View File

@@ -1,19 +1,20 @@
<template>
<x-column :column="column" :is-stacked="isStacked" :menu="menu">
<template #header><fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template>
<XColumn :column="column" :is-stacked="isStacked" :menu="menu">
<template #header><Fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template>
<x-notes :pagination="pagination" @before="before()" @after="after()"/>
</x-column>
<XNotes :pagination="pagination" @before="before()" @after="after()"/>
</XColumn>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faAt } from '@fortawesome/free-solid-svg-icons';
import Progress from '../../scripts/loading';
import Progress from '@/scripts/loading';
import XColumn from './column.vue';
import XNotes from '../notes.vue';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
XColumn,
XNotes

View File

@@ -1,19 +1,20 @@
<template>
<x-column :column="column" :is-stacked="isStacked" :menu="menu">
<template #header><fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template>
<XColumn :column="column" :is-stacked="isStacked" :menu="menu">
<template #header><Fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template>
<x-notifications/>
</x-column>
<XNotifications :include-types="column.includingTypes"/>
</XColumn>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faCog } from '@fortawesome/free-solid-svg-icons';
import { faBell } from '@fortawesome/free-regular-svg-icons';
import XColumn from './column.vue';
import XNotifications from '../notifications.vue';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
XColumn,
XNotifications
@@ -38,30 +39,21 @@ export default Vue.extend({
},
created() {
if (this.column.notificationType == null) {
this.column.notificationType = 'all';
this.$store.commit('deviceUser/updateDeckColumn', this.column);
}
this.menu = [{
icon: faCog,
text: this.$t('notificationType'),
action: () => {
this.$root.dialog({
title: this.$t('notificationType'),
type: null,
select: {
items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({
value: x, text: this.$t(`_notification._types.${x}`)
}))
default: this.column.notificationType,
text: this.$t('notificationSetting'),
action: async () => {
os.popup(await import('@/components/notification-setting-window.vue'), {
includingTypes: this.column.includingTypes,
}, {
done: async (res) => {
const { includingTypes } = res;
this.$store.commit('deviceUser/updateDeckColumn', {
...this.column,
includingTypes: includingTypes
});
},
showCancelButton: true
}).then(({ canceled, result: type }) => {
if (canceled) return;
this.column.notificationType = type;
this.$store.commit('deviceUser/updateDeckColumn', this.column);
});
}, 'closed');
}
}];
},

View File

@@ -1,31 +1,32 @@
<template>
<x-column :menu="menu" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState">
<XColumn :menu="menu" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState">
<template #header>
<fa v-if="column.tl === 'home'" :icon="faHome"/>
<fa v-else-if="column.tl === 'local'" :icon="faComments"/>
<fa v-else-if="column.tl === 'social'" :icon="faShareAlt"/>
<fa v-else-if="column.tl === 'global'" :icon="faGlobe"/>
<Fa v-if="column.tl === 'home'" :icon="faHome"/>
<Fa v-else-if="column.tl === 'local'" :icon="faComments"/>
<Fa v-else-if="column.tl === 'social'" :icon="faShareAlt"/>
<Fa v-else-if="column.tl === 'global'" :icon="faGlobe"/>
<span style="margin-left: 8px;">{{ column.name }}</span>
</template>
<div class="iwaalbte" v-if="disabled">
<p>
<fa :icon="faMinusCircle"/>
<Fa :icon="faMinusCircle"/>
{{ $t('disabled-timeline.title') }}
</p>
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
</div>
<x-timeline v-else-if="column.tl" ref="timeline" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote" :key="column.tl"/>
</x-column>
<XTimeline v-else-if="column.tl" ref="timeline" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote" :key="column.tl"/>
</XColumn>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faMinusCircle, faHome, faComments, faShareAlt, faGlobe, faCog } from '@fortawesome/free-solid-svg-icons';
import XColumn from './column.vue';
import XTimeline from '../timeline.vue';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
XColumn,
XTimeline,
@@ -78,7 +79,7 @@ export default Vue.extend({
methods: {
async setType() {
const { canceled, result: src } = await this.$root.dialog({
const { canceled, result: src } = await os.dialog({
title: this.$t('timeline'),
type: null,
select: {
@@ -99,7 +100,7 @@ export default Vue.extend({
}
return;
}
Vue.set(this.column, 'tl', src);
this.column.tl = src;
this.$store.commit('deviceUser/updateDeckColumn', this.column);
},

View File

@@ -1,47 +1,46 @@
<template>
<x-column :menu="menu" :naked="true" :column="column" :is-stacked="isStacked">
<template #header><fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template>
<XColumn :menu="menu" :naked="true" :column="column" :is-stacked="isStacked">
<template #header><Fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template>
<div class="wtdtxvec">
<template v-if="edit">
<header>
<mk-select v-model="widgetAdderSelected" style="margin-bottom: var(--margin)">
<MkSelect v-model:value="widgetAdderSelected" style="margin-bottom: var(--margin)">
<template #label>{{ $t('selectWidget') }}</template>
<option v-for="widget in widgets" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
</mk-select>
<mk-button inline @click="addWidget" primary><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
<mk-button inline @click="edit = false">{{ $t('close') }}</mk-button>
</MkSelect>
<MkButton inline @click="addWidget" primary><Fa :icon="faPlus"/> {{ $t('add') }}</MkButton>
<MkButton inline @click="edit = false">{{ $t('close') }}</MkButton>
</header>
<x-draggable
<XDraggable
:list="column.widgets"
animation="150"
@sort="onWidgetSort"
>
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)">
<button class="remove _button" @click.prevent.stop="removeWidget(widget)"><fa :icon="faTimes"/></button>
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :column="column"/>
<button class="remove _button" @click.prevent.stop="removeWidget(widget)"><Fa :icon="faTimes"/></button>
<component :is="`mkw-${widget.name}`" :widget="widget" :setting-callback="setting => settings[widget.id] = setting" :column="column"/>
</div>
</x-draggable>
</XDraggable>
</template>
<component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :column="column"/>
<component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :column="column"/>
</div>
</x-column>
</XColumn>
</template>
<script lang="ts">
import Vue from 'vue';
import * as XDraggable from 'vuedraggable';
import { defineComponent, defineAsyncComponent } from 'vue';
import { v4 as uuid } from 'uuid';
import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
import MkSelect from '../../components/ui/select.vue';
import MkButton from '../../components/ui/button.vue';
import MkSelect from '@/components/ui/select.vue';
import MkButton from '@/components/ui/button.vue';
import XColumn from './column.vue';
import { widgets } from '../../widgets';
export default Vue.extend({
export default defineComponent({
components: {
XColumn,
XDraggable,
XDraggable: defineAsyncComponent(() => import('vue-draggable-next').then(x => x.VueDraggableNext)),
MkSelect,
MkButton,
},
@@ -63,6 +62,7 @@ export default Vue.extend({
menu: null,
widgetAdderSelected: null,
widgets,
settings: {},
faWindowMaximize, faTimes, faPlus
};
},
@@ -79,7 +79,7 @@ export default Vue.extend({
methods: {
widgetFunc(id) {
this.$refs[id][0].setting();
this.settings[id]();
},
onWidgetSort() {

View File

@@ -1,69 +1,60 @@
<template>
<div class="mk-dialog" :class="{ iconOnly }">
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
<div class="bg _modalBg" ref="bg" @click="onBgClick" v-if="show"></div>
</transition>
<transition :name="$store.state.device.animation ? 'dialog' : ''" appear @after-leave="() => { destroyDom(); }">
<div class="main" ref="main" v-if="show">
<template v-if="type == 'signin'">
<mk-signin/>
<MkModal ref="modal" @click="done(true)" @closed="$emit('closed')">
<div class="mk-dialog">
<div class="icon" v-if="icon">
<Fa :icon="icon"/>
</div>
<div class="icon" v-else-if="!input && !select && !user" :class="type">
<Fa :icon="faCheck" v-if="type === 'success'"/>
<Fa :icon="faTimesCircle" v-if="type === 'error'"/>
<Fa :icon="faExclamationTriangle" v-if="type === 'warning'"/>
<Fa :icon="faInfoCircle" v-if="type === 'info'"/>
<Fa :icon="faQuestionCircle" v-if="type === 'question'"/>
<Fa :icon="faSpinner" pulse v-if="type === 'waiting'"/>
</div>
<header v-if="title" v-html="title"></header>
<header v-if="title == null && user">{{ $t('enterUsername') }}</header>
<div class="body" v-if="text" v-html="text"></div>
<MkInput v-if="input" v-model:value="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></MkInput>
<MkInput v-if="user" v-model:value="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></MkInput>
<MkSelect v-if="select" v-model:value="selectedValue" autofocus>
<template v-if="select.items">
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
</template>
<template v-else>
<div class="icon" v-if="icon">
<fa :icon="icon"/>
</div>
<div class="icon" v-else-if="!input && !select && !user" :class="type">
<fa :icon="faCheck" v-if="type === 'success'"/>
<fa :icon="faTimesCircle" v-if="type === 'error'"/>
<fa :icon="faExclamationTriangle" v-if="type === 'warning'"/>
<fa :icon="faInfoCircle" v-if="type === 'info'"/>
<fa :icon="faQuestionCircle" v-if="type === 'question'"/>
<fa :icon="faSpinner" pulse v-if="type === 'waiting'"/>
</div>
<header v-if="title" v-html="title"></header>
<header v-if="title == null && user">{{ $t('enterUsername') }}</header>
<div class="body" v-if="text" v-html="text"></div>
<mk-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></mk-input>
<mk-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></mk-input>
<mk-select v-if="select" v-model="selectedValue" autofocus>
<template v-if="select.items">
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
</template>
<template v-else>
<optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
</optgroup>
</template>
</mk-select>
<div class="buttons" v-if="!iconOnly && (showOkButton || showCancelButton) && !actions">
<mk-button inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('ok') : $t('gotIt') }}</mk-button>
<mk-button inline @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('cancel') }}</mk-button>
</div>
<div class="buttons" v-if="actions">
<mk-button v-for="action in actions" inline @click="() => { action.callback(); close(); }" :primary="action.primary" :key="action.text">{{ action.text }}</mk-button>
</div>
<optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
</optgroup>
</template>
</MkSelect>
<div class="buttons" v-if="(showOkButton || showCancelButton) && !actions">
<MkButton inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('ok') : $t('gotIt') }}</MkButton>
<MkButton inline @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('cancel') }}</MkButton>
</div>
</transition>
</div>
<div class="buttons" v-if="actions">
<MkButton v-for="action in actions" inline @click="() => { action.callback(); close(); }" :primary="action.primary" :key="action.text">{{ action.text }}</MkButton>
</div>
</div>
</MkModal>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faSpinner, faInfoCircle, faExclamationTriangle, faCheck } from '@fortawesome/free-solid-svg-icons';
import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons';
import MkButton from './ui/button.vue';
import MkInput from './ui/input.vue';
import MkSelect from './ui/select.vue';
import MkSignin from './signin.vue';
import MkModal from '@/components/ui/modal.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import MkSelect from '@/components/ui/select.vue';
import parseAcct from '../../misc/acct/parse';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
MkModal,
MkButton,
MkInput,
MkSelect,
MkSignin,
},
props: {
@@ -107,19 +98,12 @@ export default Vue.extend({
type: Boolean,
default: true
},
iconOnly: {
type: Boolean,
default: false
},
autoClose: {
type: Boolean,
default: false
}
},
emits: ['done', 'closed'],
data() {
return {
show: true,
inputValue: this.input && this.input.default ? this.input.default : null,
userInputValue: null,
selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null,
@@ -131,63 +115,51 @@ export default Vue.extend({
watch: {
userInputValue() {
if (this.user) {
this.$root.api('users/show', parseAcct(this.userInputValue)).then(u => {
os.api('users/show', parseAcct(this.userInputValue)).then(u => {
this.canOk = u != null;
}).catch(() => {
this.canOk = false;
});
}
}
},
},
mounted() {
if (this.user) this.canOk = false;
if (this.autoClose) {
setTimeout(() => {
this.close();
}, 1000);
}
document.addEventListener('keydown', this.onKeydown);
},
beforeDestroy() {
beforeUnmount() {
document.removeEventListener('keydown', this.onKeydown);
},
methods: {
done(canceled, result?) {
this.$emit('done', { canceled, result });
this.$refs.modal.close();
},
async ok() {
if (!this.canOk) return;
if (!this.showOkButton) return;
if (this.user) {
const user = await this.$root.api('users/show', parseAcct(this.userInputValue));
const user = await os.api('users/show', parseAcct(this.userInputValue));
if (user) {
this.$emit('ok', user);
this.close();
this.done(false, user);
}
} else {
const result =
this.input ? this.inputValue :
this.select ? this.selectedValue :
true;
this.$emit('ok', result);
this.close();
this.done(false, result);
}
},
cancel() {
this.$emit('cancel');
this.close();
},
close() {
if (!this.show) return;
this.show = false;
this.$el.style.pointerEvents = 'none';
(this.$refs.bg as any).style.pointerEvents = 'none';
(this.$refs.main as any).style.pointerEvents = 'none';
this.done(true);
},
onBgClick() {
@@ -214,95 +186,60 @@ export default Vue.extend({
</script>
<style lang="scss" scoped>
.dialog-enter-active, .dialog-leave-active {
transition: opacity 0.3s, transform 0.3s !important;
}
.dialog-enter, .dialog-leave-to {
opacity: 0;
transform: scale(0.9);
}
.bg-fade-enter-active, .bg-fade-leave-active {
transition: opacity 0.3s !important;
}
.bg-fade-enter, .bg-fade-leave-to {
opacity: 0;
}
.mk-dialog {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
z-index: 30000;
top: 0;
left: 0;
width: 100%;
height: 100%;
position: relative;
padding: 32px;
min-width: 320px;
max-width: 480px;
box-sizing: border-box;
text-align: center;
background: var(--panel);
border-radius: var(--radius);
&.iconOnly > .main {
min-width: 0;
width: initial;
> .icon {
font-size: 32px;
&.success {
color: var(--accent);
}
&.error {
color: #ec4137;
}
&.warning {
color: #ecb637;
}
> * {
display: block;
margin: 0 auto;
}
& + header {
margin-top: 16px;
}
}
> .main {
display: block;
position: fixed;
margin: auto;
padding: 32px;
min-width: 320px;
max-width: 480px;
box-sizing: border-box;
width: calc(100% - 32px);
text-align: center;
background: var(--panel);
border-radius: var(--radius);
> header {
margin: 0 0 8px 0;
font-weight: bold;
font-size: 20px;
> .icon {
font-size: 32px;
&.success {
color: var(--accent);
}
&.error {
color: #ec4137;
}
&.warning {
color: #ecb637;
}
> * {
display: block;
margin: 0 auto;
}
& + header {
margin-top: 16px;
}
& + .body {
margin-top: 8px;
}
}
> header {
margin: 0 0 8px 0;
font-weight: bold;
font-size: 20px;
> .body {
margin: 16px 0 0 0;
}
& + .body {
margin-top: 8px;
}
}
> .buttons {
margin-top: 16px;
> .body {
margin: 16px 0 0 0;
}
> .buttons {
margin-top: 16px;
> * {
margin: 0 8px;
}
> * {
margin: 0 8px;
}
}
}

View File

@@ -1,20 +1,20 @@
<template>
<div class="zdjebgpv" ref="thumbnail">
<img-with-blurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/>
<fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/>
<fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/>
<fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/>
<fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/>
<fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/>
<fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/>
<fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/>
<fa :icon="faFile" class="icon" v-else/>
<fa :icon="faFilm" class="icon-sub" v-if="isThumbnailAvailable && is === 'video'"/>
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/>
<Fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/>
<Fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/>
<Fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/>
<Fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/>
<Fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/>
<Fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/>
<Fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/>
<Fa :icon="faFile" class="icon" v-else/>
<Fa :icon="faFilm" class="icon-sub" v-if="isThumbnailAvailable && is === 'video'"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import {
faFile,
faFileAlt,
@@ -28,7 +28,7 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import ImgWithBlurhash from './img-with-blurhash.vue';
export default Vue.extend({
export default defineComponent({
components: {
ImgWithBlurhash
},

View File

@@ -1,31 +1,41 @@
<template>
<x-window ref="window" :width="800" :height="500" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="(type === 'file') && (selected.length === 0)" @ok="ok()">
<XModalWindow ref="dialog"
:width="800"
:height="500"
:with-ok-button="true"
:ok-button-disabled="(type === 'file') && (selected.length === 0)"
@click="cancel()"
@close="cancel()"
@ok="ok()"
@closed="$emit('closed')"
>
<template #header>
{{ multiple ? ((type === 'file') ? $t('selectFiles') : $t('selectFolders')) : ((type === 'file') ? $t('selectFile') : $t('selectFolder')) }}
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ selected.length | number }})</span>
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
</template>
<div>
<x-drive :multiple="multiple" @change-selection="onChangeSelection" :select="type"/>
<XDrive :multiple="multiple" @changeSelection="onChangeSelection" @selected="ok()" :select="type"/>
</div>
</x-window>
</XModalWindow>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import XDrive from './drive.vue';
import XWindow from './window.vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import number from '@/filters/number';
export default Vue.extend({
export default defineComponent({
components: {
XDrive,
XWindow,
XModalWindow,
},
props: {
type: {
type: String,
required: false,
default: 'file'
default: 'file'
},
multiple: {
type: Boolean,
@@ -33,6 +43,8 @@ export default Vue.extend({
}
},
emits: ['done', 'closed'],
data() {
return {
selected: []
@@ -41,13 +53,20 @@ export default Vue.extend({
methods: {
ok() {
this.$emit('selected', this.selected);
this.$refs.window.close();
this.$emit('done', this.selected);
this.$refs.dialog.close();
},
cancel() {
this.$emit('done');
this.$refs.dialog.close();
},
onChangeSelection(xs) {
this.selected = xs;
}
},
number
}
});
</script>

View File

@@ -1,7 +1,8 @@
<template>
<div class="ncvczrfv"
:data-is-selected="isSelected"
:class="{ isSelected }"
@click="onClick"
@contextmenu.stop="onContextmenu"
draggable="true"
@dragstart="onDragstart"
@dragend="onDragend"
@@ -20,7 +21,7 @@
<p>{{ $t('nsfw') }}</p>
</div>
<x-file-thumbnail class="thumbnail" :file="file" fit="contain"/>
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
<p class="name">
<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
@@ -30,17 +31,17 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
import copyToClipboard from '../scripts/copy-to-clipboard';
//import updateAvatar from '../api/update-avatar';
//import updateBanner from '../api/update-banner';
import XFileThumbnail from './drive-file-thumbnail.vue';
import { faDownload, faLink, faICursor, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
import bytes from '../filters/bytes';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
XFileThumbnail
MkDriveFileThumbnail
},
props: {
@@ -60,6 +61,8 @@ export default Vue.extend({
}
},
emits: ['chosen'],
data() {
return {
isDragging: false
@@ -72,48 +75,54 @@ export default Vue.extend({
return this.$parent;
},
title(): string {
return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.size)}`;
return `${this.file.name}\n${this.file.type} ${bytes(this.file.size)}`;
}
},
methods: {
getMenu() {
return [{
text: this.$t('rename'),
icon: faICursor,
action: this.rename
}, {
text: this.file.isSensitive ? this.$t('unmarkAsSensitive') : this.$t('markAsSensitive'),
icon: this.file.isSensitive ? faEye : faEyeSlash,
action: this.toggleSensitive
}, null, {
text: this.$t('copyUrl'),
icon: faLink,
action: this.copyUrl
}, {
type: 'a',
href: this.file.url,
target: '_blank',
text: this.$t('download'),
icon: faDownload,
download: this.file.name
}, null, {
text: this.$t('delete'),
icon: faTrashAlt,
danger: true,
action: this.deleteFile
}];
},
onClick(ev) {
if (this.selectMode) {
this.$emit('chosen', this.file);
} else {
this.$root.menu({
items: [{
text: this.$t('rename'),
icon: faICursor,
action: this.rename
}, {
text: this.file.isSensitive ? this.$t('unmarkAsSensitive') : this.$t('markAsSensitive'),
icon: this.file.isSensitive ? faEye : faEyeSlash,
action: this.toggleSensitive
}, null, {
text: this.$t('copyUrl'),
icon: faLink,
action: this.copyUrl
}, {
type: 'a',
href: this.file.url,
target: '_blank',
text: this.$t('download'),
icon: faDownload,
download: this.file.name
}, null, {
text: this.$t('delete'),
icon: faTrashAlt,
action: this.deleteFile
}],
source: ev.currentTarget || ev.target,
});
os.modalMenu(this.getMenu(), ev.currentTarget || ev.target);
}
},
onContextmenu(e) {
os.contextMenu(this.getMenu(), e);
},
onDragstart(e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('mk_drive_file', JSON.stringify(this.file));
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(this.file));
this.isDragging = true;
// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
@@ -127,7 +136,7 @@ export default Vue.extend({
},
rename() {
this.$root.dialog({
os.dialog({
title: this.$t('renameFile'),
input: {
placeholder: this.$t('inputNewFileName'),
@@ -136,7 +145,7 @@ export default Vue.extend({
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$root.api('drive/files/update', {
os.api('drive/files/update', {
fileId: this.file.id,
name: name
});
@@ -144,7 +153,7 @@ export default Vue.extend({
},
toggleSensitive() {
this.$root.api('drive/files/update', {
os.api('drive/files/update', {
fileId: this.file.id,
isSensitive: !this.file.isSensitive
});
@@ -152,18 +161,15 @@ export default Vue.extend({
copyUrl() {
copyToClipboard(this.file.url);
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
os.success();
},
setAsAvatar() {
updateAvatar(this.$root)(this.file);
os.updateAvatar(this.file);
},
setAsBanner() {
updateBanner(this.$root)(this.file);
os.updateBanner(this.file);
},
addApp() {
@@ -171,17 +177,19 @@ export default Vue.extend({
},
async deleteFile() {
const { canceled } = await this.$root.dialog({
const { canceled } = await os.dialog({
type: 'warning',
text: this.$t('driveFileDeleteConfirm', { name: this.file.name }),
showCancelButton: true
});
if (canceled) return;
this.$root.api('drive/files/delete', {
os.api('drive/files/delete', {
fileId: this.file.id
});
}
},
bytes
}
});
</script>
@@ -197,6 +205,10 @@ export default Vue.extend({
cursor: pointer;
}
> * {
pointer-events: none;
}
&:hover {
background: rgba(#000, 0.05);
@@ -233,7 +245,7 @@ export default Vue.extend({
}
}
&[data-is-selected] {
&.isSelected {
background: var(--accent);
&:hover {

View File

@@ -1,6 +1,6 @@
<template>
<div class="rghtznwe"
:data-draghover="draghover"
:class="{ draghover }"
@click="onClick"
@mouseover="onMouseover"
@mouseout="onMouseout"
@@ -14,8 +14,8 @@
:title="title"
>
<p class="name">
<template v-if="hover"><fa :icon="faFolderOpen" fixed-width/></template>
<template v-if="!hover"><fa :icon="faFolder" fixed-width/></template>
<template v-if="hover"><Fa :icon="faFolderOpen" fixed-width/></template>
<template v-if="!hover"><Fa :icon="faFolder" fixed-width/></template>
{{ folder.name }}
</p>
<p class="upload" v-if="$store.state.settings.uploadFolder == folder.id">
@@ -26,10 +26,11 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faFolder, faFolderOpen } from '@fortawesome/free-regular-svg-icons';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
folder: {
type: Object,
@@ -47,6 +48,8 @@ export default Vue.extend({
}
},
emits: ['chosen'],
data() {
return {
hover: false,
@@ -91,8 +94,8 @@ export default Vue.extend({
}
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
if (isFile || isDriveFile || isDriveFolder) {
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
@@ -121,11 +124,11 @@ export default Vue.extend({
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData('mk_drive_file');
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile);
this.browser.removeFile(file.id);
this.$root.api('drive/files/update', {
os.api('drive/files/update', {
fileId: file.id,
folderId: this.folder.id
});
@@ -133,7 +136,7 @@ export default Vue.extend({
//#endregion
//#region ドライブのフォルダ
const driveFolder = e.dataTransfer.getData('mk_drive_folder');
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder != '') {
const folder = JSON.parse(driveFolder);
@@ -141,7 +144,7 @@ export default Vue.extend({
if (folder.id == this.folder.id) return;
this.browser.removeFolder(folder.id);
this.$root.api('drive/folders/update', {
os.api('drive/folders/update', {
folderId: folder.id,
parentId: this.folder.id
}).then(() => {
@@ -149,15 +152,15 @@ export default Vue.extend({
}).catch(err => {
switch (err) {
case 'detected-circular-definition':
this.$root.dialog({
os.dialog({
title: this.$t('unableToProcess'),
text: this.$t('circularReferenceFolder')
});
break;
default:
this.$root.dialog({
os.dialog({
type: 'error',
text: this.$t('error')
text: this.$t('somethingHappened')
});
}
});
@@ -167,7 +170,7 @@ export default Vue.extend({
onDragstart(e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('mk_drive_folder', JSON.stringify(this.folder));
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(this.folder));
this.isDragging = true;
// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
@@ -189,7 +192,7 @@ export default Vue.extend({
},
rename() {
this.$root.dialog({
os.dialog({
title: this.$t('renameFolder'),
input: {
placeholder: this.$t('inputNewFolderName'),
@@ -197,7 +200,7 @@ export default Vue.extend({
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$root.api('drive/folders/update', {
os.api('drive/folders/update', {
folderId: this.folder.id,
name: name
});
@@ -205,7 +208,7 @@ export default Vue.extend({
},
deleteFolder() {
this.$root.api('drive/folders/delete', {
os.api('drive/folders/delete', {
folderId: this.folder.id
}).then(() => {
if (this.$store.state.settings.uploadFolder === this.folder.id) {
@@ -217,14 +220,14 @@ export default Vue.extend({
}).catch(err => {
switch(err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
this.$root.dialog({
os.dialog({
type: 'error',
title: this.$t('unableToDelete'),
text: this.$t('hasChildFilesOrFolders')
});
break;
default:
this.$root.dialog({
os.dialog({
type: 'error',
text: this.$t('unableToDelete')
});
@@ -272,7 +275,7 @@ export default Vue.extend({
}
}
&[data-draghover] {
&.draghover {
&:after {
content: "";
pointer-events: none;

View File

@@ -1,22 +1,23 @@
<template>
<div class="drylbebk"
:data-draghover="draghover"
:class="{ draghover }"
@click="onClick"
@dragover.prevent.stop="onDragover"
@dragenter="onDragenter"
@dragleave="onDragleave"
@drop.stop="onDrop"
>
<i v-if="folder == null"><fa :icon="faCloud"/></i>
<i v-if="folder == null"><Fa :icon="faCloud"/></i>
<span>{{ folder == null ? $t('drive') : folder.name }}</span>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faCloud } from '@fortawesome/free-solid-svg-icons';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
folder: {
type: Object,
@@ -58,8 +59,8 @@ export default Vue.extend({
}
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
if (isFile || isDriveFile || isDriveFolder) {
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
@@ -90,11 +91,11 @@ export default Vue.extend({
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData('mk_drive_file');
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile);
this.browser.removeFile(file.id);
this.$root.api('drive/files/update', {
os.api('drive/files/update', {
fileId: file.id,
folderId: this.folder ? this.folder.id : null
});
@@ -102,13 +103,13 @@ export default Vue.extend({
//#endregion
//#region ドライブのフォルダ
const driveFolder = e.dataTransfer.getData('mk_drive_folder');
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder != '') {
const folder = JSON.parse(driveFolder);
// 移動先が自分自身ならreject
if (this.folder && folder.id == this.folder.id) return;
this.browser.removeFolder(folder.id);
this.$root.api('drive/folders/update', {
os.api('drive/folders/update', {
folderId: folder.id,
parentId: this.folder ? this.folder.id : null
});
@@ -125,7 +126,7 @@ export default Vue.extend({
pointer-events: none;
}
&[data-draghover] {
&.draghover {
background: #eee;
}

View File

@@ -2,34 +2,35 @@
<div class="yfudmmck">
<nav>
<div class="path" @contextmenu.prevent.stop="() => {}">
<x-nav-folder :class="{ current: folder == null }"/>
<XNavFolder :class="{ current: folder == null }"/>
<template v-for="f in hierarchyFolders">
<span class="separator" :key="f.id + ':separator'"><fa :icon="faAngleRight"/></span>
<x-nav-folder :folder="f" :key="f.id"/>
<span class="separator"><Fa :icon="faAngleRight"/></span>
<XNavFolder :folder="f"/>
</template>
<span class="separator" v-if="folder != null"><fa :icon="faAngleRight"/></span>
<span class="separator" v-if="folder != null"><Fa :icon="faAngleRight"/></span>
<span class="folder current" v-if="folder != null">{{ folder.name }}</span>
</div>
</nav>
<div class="main" :class="{ uploading: uploadings.length > 0, fetching }"
<div class="main _section" :class="{ uploading: uploadings.length > 0, fetching }"
ref="main"
@dragover.prevent.stop="onDragover"
@dragenter="onDragenter"
@dragleave="onDragleave"
@drop.prevent.stop="onDrop"
@contextmenu="onContextmenu"
>
<div class="contents" ref="contents">
<div class="folders" ref="foldersContainer" v-show="folders.length > 0">
<x-folder v-for="f in folders" :key="f.id" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/>
<XFolder v-for="f in folders" :key="f.id" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/>
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div class="padding" v-for="(n, i) in 16" :key="i"></div>
<mk-button ref="moreFolders" v-if="moreFolders">{{ $t('loadMore') }}</mk-button>
<MkButton ref="moreFolders" v-if="moreFolders">{{ $t('loadMore') }}</MkButton>
</div>
<div class="files" ref="filesContainer" v-show="files.length > 0">
<x-file v-for="file in files" :key="file.id" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/>
<XFile v-for="file in files" :key="file.id" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/>
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div class="padding" v-for="(n, i) in 16" :key="i"></div>
<mk-button ref="loadMoreFiles" @click="fetchMoreFiles" v-show="moreFiles">{{ $t('loadMore') }}</mk-button>
<MkButton ref="loadMoreFiles" @click="fetchMoreFiles" v-show="moreFiles">{{ $t('loadMore') }}</MkButton>
</div>
<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
<p v-if="draghover">{{ $t('empty-draghover') }}</p>
@@ -37,29 +38,28 @@
<p v-if="!draghover && folder != null">{{ $t('emptyFolder') }}</p>
</div>
</div>
<mk-loading v-if="fetching"/>
<MkLoading v-if="fetching"/>
</div>
<div class="dropzone" v-if="draghover"></div>
<x-uploader ref="uploader" @change="onChangeUploaderUploads" @uploaded="onUploaderUploaded"/>
<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { defineComponent } from 'vue';
import { faAngleRight, faFolderPlus, faICursor, faLink, faUpload } from '@fortawesome/free-solid-svg-icons';
import XNavFolder from './drive.nav-folder.vue';
import XFolder from './drive.folder.vue';
import XFile from './drive.file.vue';
import XUploader from './uploader.vue';
import MkButton from './ui/button.vue';
import * as os from '@/os';
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
export default defineComponent({
components: {
XNavFolder,
XFolder,
XFile,
XUploader,
MkButton,
},
@@ -85,6 +85,8 @@ export default Vue.extend({
}
},
emits: ['selected', 'change-selection', 'move-root', 'cd', 'open-folder'],
data() {
return {
/**
@@ -100,7 +102,7 @@ export default Vue.extend({
hierarchyFolders: [],
selectedFiles: [],
selectedFolders: [],
uploadings: [],
uploadings: os.uploads,
connection: null,
/**
@@ -140,7 +142,7 @@ export default Vue.extend({
});
}
this.connection = this.$root.stream.useSharedConnection('drive');
this.connection = os.stream.useSharedConnection('drive');
this.connection.on('fileCreated', this.onStreamDriveFileCreated);
this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);
@@ -164,7 +166,7 @@ export default Vue.extend({
}
},
beforeDestroy() {
beforeUnmount() {
this.connection.dispose();
this.ilFilesObserver.disconnect();
},
@@ -204,14 +206,6 @@ export default Vue.extend({
this.removeFolder(folderId);
},
onChangeUploaderUploads(uploads) {
this.uploadings = uploads;
},
onUploaderUploaded(file) {
this.addFile(file, true);
},
onDragover(e): any {
// ドラッグ元が自分自身の所有するアイテムだったら
if (this.isDragSource) {
@@ -221,8 +215,8 @@ export default Vue.extend({
}
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
if (isFile || isDriveFile || isDriveFolder) {
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
@@ -253,12 +247,12 @@ export default Vue.extend({
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData('mk_drive_file');
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile);
if (this.files.some(f => f.id == file.id)) return;
this.removeFile(file.id);
this.$root.api('drive/files/update', {
os.api('drive/files/update', {
fileId: file.id,
folderId: this.folder ? this.folder.id : null
});
@@ -266,7 +260,7 @@ export default Vue.extend({
//#endregion
//#region ドライブのフォルダ
const driveFolder = e.dataTransfer.getData('mk_drive_folder');
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder != '') {
const folder = JSON.parse(driveFolder);
@@ -274,7 +268,7 @@ export default Vue.extend({
if (this.folder && folder.id == this.folder.id) return false;
if (this.folders.some(f => f.id == folder.id)) return false;
this.removeFolder(folder.id);
this.$root.api('drive/folders/update', {
os.api('drive/folders/update', {
folderId: folder.id,
parentId: this.folder ? this.folder.id : null
}).then(() => {
@@ -282,15 +276,15 @@ export default Vue.extend({
}).catch(err => {
switch (err) {
case 'detected-circular-definition':
this.$root.dialog({
os.dialog({
title: this.$t('unableToProcess'),
text: this.$t('circularReferenceFolder')
});
break;
default:
this.$root.dialog({
os.dialog({
type: 'error',
text: this.$t('error')
text: this.$t('somethingHappened')
});
}
});
@@ -303,19 +297,19 @@ export default Vue.extend({
},
urlUpload() {
this.$root.dialog({
os.dialog({
title: this.$t('uploadFromUrl'),
input: {
placeholder: this.$t('uploadFromUrlDescription')
}
}).then(({ canceled, result: url }) => {
if (canceled) return;
this.$root.api('drive/files/upload_from_url', {
os.api('drive/files/upload_from_url', {
url: url,
folderId: this.folder ? this.folder.id : undefined
});
this.$root.dialog({
os.dialog({
title: this.$t('uploadFromUrlRequested'),
text: this.$t('uploadFromUrlMayTakeTime')
});
@@ -323,14 +317,14 @@ export default Vue.extend({
},
createFolder() {
this.$root.dialog({
os.dialog({
title: this.$t('createFolder'),
input: {
placeholder: this.$t('folderName')
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$root.api('drive/folders/create', {
os.api('drive/folders/create', {
name: name,
parentId: this.folder ? this.folder.id : undefined
}).then(folder => {
@@ -340,7 +334,7 @@ export default Vue.extend({
},
renameFolder(folder) {
this.$root.dialog({
os.dialog({
title: this.$t('renameFolder'),
input: {
placeholder: this.$t('inputNewFolderName'),
@@ -348,7 +342,7 @@ export default Vue.extend({
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$root.api('drive/folders/update', {
os.api('drive/folders/update', {
folderId: folder.id,
name: name
}).then(folder => {
@@ -359,7 +353,7 @@ export default Vue.extend({
},
deleteFolder(folder) {
this.$root.api('drive/folders/delete', {
os.api('drive/folders/delete', {
folderId: folder.id
}).then(() => {
// 削除時に親フォルダに移動
@@ -367,14 +361,14 @@ export default Vue.extend({
}).catch(err => {
switch(err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
this.$root.dialog({
os.dialog({
type: 'error',
title: this.$t('unableToDelete'),
text: this.$t('hasChildFilesOrFolders')
});
break;
default:
this.$root.dialog({
os.dialog({
type: 'error',
text: this.$t('unableToDelete')
});
@@ -390,7 +384,9 @@ export default Vue.extend({
upload(file, folder) {
if (folder && typeof folder == 'object') folder = folder.id;
(this.$refs.uploader as any).upload(file, folder);
os.upload(file, folder).then(res => {
this.addFile(res, true);
});
},
chooseFile(file) {
@@ -441,7 +437,7 @@ export default Vue.extend({
this.fetching = true;
this.$root.api('drive/folders/show', {
os.api('drive/folders/show', {
folderId: target
}).then(folder => {
this.folder = folder;
@@ -465,7 +461,7 @@ export default Vue.extend({
if (this.folders.some(f => f.id == folder.id)) {
const exist = this.folders.map(f => f.id).indexOf(folder.id);
Vue.set(this.folders, exist, folder);
this.folders[exist] = folder;
return;
}
@@ -482,7 +478,7 @@ export default Vue.extend({
if (this.files.some(f => f.id == file.id)) {
const exist = this.files.map(f => f.id).indexOf(file.id);
Vue.set(this.files, exist, file);
this.files[exist] = file;
return;
}
@@ -543,7 +539,7 @@ export default Vue.extend({
const filesMax = 30;
// フォルダ一覧取得
this.$root.api('drive/folders', {
os.api('drive/folders', {
folderId: this.folder ? this.folder.id : null,
limit: foldersMax + 1
}).then(folders => {
@@ -556,7 +552,7 @@ export default Vue.extend({
});
// ファイル一覧取得
this.$root.api('drive/files', {
os.api('drive/files', {
folderId: this.folder ? this.folder.id : null,
type: this.type,
limit: filesMax + 1
@@ -587,7 +583,7 @@ export default Vue.extend({
const max = 30;
// ファイル一覧取得
this.$root.api('drive/files', {
os.api('drive/files', {
folderId: this.folder ? this.folder.id : null,
type: this.type,
untilId: this.files[this.files.length - 1].id,
@@ -602,7 +598,41 @@ export default Vue.extend({
for (const x of files) this.appendFile(x);
this.fetching = false;
});
}
},
getMenu() {
return [{
text: this.$t('addFile'),
type: 'label'
}, {
text: this.$t('upload'),
icon: faUpload,
action: () => { this.selectLocalFile(); }
}, {
text: this.$t('fromUrl'),
icon: faLink,
action: () => { this.urlUpload(); }
}, null, {
text: this.folder ? this.folder.name : this.$t('drive'),
type: 'label'
}, this.folder ? {
text: this.$t('renameFolder'),
icon: faICursor,
action: () => { this.renameFolder(this.folder); }
} : undefined, this.folder ? {
text: this.$t('deleteFolder'),
icon: faTrashAlt,
action: () => { this.deleteFolder(this.folder); }
} : undefined, {
text: this.$t('createFolder'),
icon: faFolderPlus,
action: () => { this.createFolder(); }
}];
},
onContextmenu(e) {
os.contextMenu(this.getMenu(), e);
},
}
});
</script>
@@ -613,6 +643,8 @@ export default Vue.extend({
display: block;
z-index: 2;
width: 100%;
padding: 0 8px;
box-sizing: border-box;
overflow: auto;
font-size: 0.9em;
box-shadow: 0 1px 0 var(--divider);
@@ -666,7 +698,6 @@ export default Vue.extend({
}
> .main {
padding: 8px 0;
overflow: auto;
&, * {
@@ -734,11 +765,6 @@ export default Vue.extend({
pointer-events: none;
}
> .mk-uploader {
height: 100px;
padding: 16px;
}
> input {
display: none;
}

View File

@@ -1,6 +1,6 @@
<template>
<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }">
<div class="omfetrab">
<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
<div class="omfetrab _popup">
<header>
<button v-for="(category, i) in categories"
class="_button"
@@ -8,26 +8,26 @@
:class="{ active: category.isActive }"
:key="i"
>
<fa :icon="category.icon" fixed-width/>
<Fa :icon="category.icon" fixed-width/>
</button>
</header>
<div class="emojis">
<template v-if="categories[0].isActive">
<header class="category"><fa :icon="faHistory" fixed-width/> {{ $t('recentUsed') }}</header>
<header class="category"><Fa :icon="faHistory" fixed-width/> {{ $t('recentUsed') }}</header>
<div class="list">
<button v-for="(emoji, i) in ($store.state.device.recentEmojis || [])"
<button v-for="emoji in ($store.state.device.recentEmojis || [])"
class="_button"
:title="emoji.name"
@click="chosen(emoji)"
:key="i"
:key="emoji"
>
<mk-emoji v-if="emoji.char != null" :emoji="emoji.char"/>
<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
<img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
</button>
</div>
<header class="category"><fa :icon="faAsterisk" fixed-width/> {{ $t('customEmojis') }}</header>
<header class="category"><Fa :icon="faAsterisk" fixed-width/> {{ $t('customEmojis') }}</header>
</template>
<template v-if="categories.find(x => x.isActive).name">
@@ -38,7 +38,7 @@
@click="chosen(emoji)"
:key="emoji.name"
>
<mk-emoji :emoji="emoji.char"/>
<MkEmoji :emoji="emoji.char"/>
</button>
</div>
</template>
@@ -59,29 +59,31 @@
</template>
</div>
</div>
</x-popup>
</MkModal>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { emojilist } from '../../misc/emojilist';
import { getStaticImageUrl } from '../scripts/get-static-image-url';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory, faUser } from '@fortawesome/free-solid-svg-icons';
import { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons';
import { groupByX } from '../../prelude/array';
import XPopup from './popup.vue';
import MkModal from '@/components/ui/modal.vue';
export default Vue.extend({
export default defineComponent({
components: {
XPopup,
MkModal,
},
props: {
source: {
required: true
src: {
required: false
},
},
emits: ['done', 'closed'],
data() {
return {
emojilist,
@@ -162,12 +164,9 @@ export default Vue.extend({
recents = recents.filter((e: any) => getKey(e) !== getKey(emoji));
recents.unshift(emoji)
this.$store.commit('device/set', { key: 'recentEmojis', value: recents.splice(0, 16) });
this.$emit('chosen', getKey(emoji));
this.$emit('done', getKey(emoji));
this.$refs.modal.close();
},
close() {
this.$refs.popup.close();
}
}
});
</script>

View File

@@ -6,11 +6,12 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { getStaticImageUrl } from '../scripts/get-static-image-url';
import { defineComponent } from 'vue';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { twemojiSvgBase } from '../../misc/twemoji-base';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
name: {
type: String,

View File

@@ -1,19 +1,19 @@
<template>
<transition :name="$store.state.device.animation ? 'zoom' : ''" appear>
<div class="mjndxjcg _panel">
<div class="mjndxjcg">
<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
<p><fa :icon="faExclamationTriangle"/> {{ $t('error') }}</p>
<mk-button @click="() => $emit('retry')" class="button">{{ $t('retry') }}</mk-button>
<p><Fa :icon="faExclamationTriangle"/> {{ $t('somethingHappened') }}</p>
<MkButton @click="() => $emit('retry')" class="button">{{ $t('retry') }}</MkButton>
</div>
</transition>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import MkButton from './ui/button.vue';
export default Vue.extend({
export default defineComponent({
components: {
MkButton,
},

View File

@@ -1,14 +1,15 @@
<template>
<span class="mk-file-type-icon">
<template v-if="kind == 'image'"><fa :icon="faFileImage"/></template>
<template v-if="kind == 'image'"><Fa :icon="faFileImage"/></template>
</span>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faFileImage } from '@fortawesome/free-solid-svg-icons';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
type: {
type: String,

View File

@@ -7,32 +7,33 @@
>
<template v-if="!wait">
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
<span v-if="full">{{ $t('followRequestPending') }}</span><fa :icon="faHourglassHalf"/>
<span v-if="full">{{ $t('followRequestPending') }}</span><Fa :icon="faHourglassHalf"/>
</template>
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合 -->
<span v-if="full">{{ $t('processing') }}</span><fa :icon="faSpinner" pulse/>
<span v-if="full">{{ $t('processing') }}</span><Fa :icon="faSpinner" pulse/>
</template>
<template v-else-if="isFollowing">
<span v-if="full">{{ $t('unfollow') }}</span><fa :icon="faMinus"/>
<span v-if="full">{{ $t('unfollow') }}</span><Fa :icon="faMinus"/>
</template>
<template v-else-if="!isFollowing && user.isLocked">
<span v-if="full">{{ $t('followRequest') }}</span><fa :icon="faPlus"/>
<span v-if="full">{{ $t('followRequest') }}</span><Fa :icon="faPlus"/>
</template>
<template v-else-if="!isFollowing && !user.isLocked">
<span v-if="full">{{ $t('follow') }}</span><fa :icon="faPlus"/>
<span v-if="full">{{ $t('follow') }}</span><Fa :icon="faPlus"/>
</template>
</template>
<template v-else>
<span v-if="full">{{ $t('processing') }}</span><fa :icon="faSpinner" pulse fixed-width/>
<span v-if="full">{{ $t('processing') }}</span><Fa :icon="faSpinner" pulse fixed-width/>
</template>
</button>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faSpinner, faPlus, faMinus, faHourglassHalf } from '@fortawesome/free-solid-svg-icons';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
user: {
type: Object,
@@ -58,7 +59,7 @@ export default Vue.extend({
created() {
// 渡されたユーザー情報が不完全な場合
if (this.user.isFollowing == null) {
this.$root.api('users/show', {
os.api('users/show', {
userId: this.user.id
}).then(u => {
this.isFollowing = u.isFollowing;
@@ -68,13 +69,13 @@ export default Vue.extend({
},
mounted() {
this.connection = this.$root.stream.useSharedConnection('main');
this.connection = os.stream.useSharedConnection('main');
this.connection.on('follow', this.onFollowChange);
this.connection.on('unfollow', this.onFollowChange);
},
beforeDestroy() {
beforeUnmount() {
this.connection.dispose();
},
@@ -91,7 +92,7 @@ export default Vue.extend({
try {
if (this.isFollowing) {
const { canceled } = await this.$root.dialog({
const { canceled } = await os.dialog({
type: 'warning',
text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }),
showCancelButton: true
@@ -99,21 +100,21 @@ export default Vue.extend({
if (canceled) return;
await this.$root.api('following/delete', {
await os.api('following/delete', {
userId: this.user.id
});
} else {
if (this.hasPendingFollowRequestFromYou) {
await this.$root.api('following/requests/cancel', {
await os.api('following/requests/cancel', {
userId: this.user.id
});
} else if (this.user.isLocked) {
await this.$root.api('following/create', {
await os.api('following/create', {
userId: this.user.id
});
this.hasPendingFollowRequestFromYou = true;
} else {
await this.$root.api('following/create', {
await os.api('following/create', {
userId: this.user.id
});
this.hasPendingFollowRequestFromYou = true;

View File

@@ -1,41 +1,50 @@
<template>
<x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" :can-close="false">
<XModalWindow ref="dialog"
:width="400"
:can-close="false"
:with-ok-button="true"
:ok-button-disabled="false"
@click="cancel()"
@ok="ok()"
@close="cancel()"
@closed="$emit('closed')"
>
<template #header>
{{ title }}
</template>
<div class="xkpnjxcv">
<div class="xkpnjxcv _section">
<label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item">
<mk-input v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
<MkInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</mk-input>
<mk-input v-else-if="form[item].type === 'string' && !item.multiline" v-model="values[item]" type="text">
</MkInput>
<MkInput v-else-if="form[item].type === 'string' && !item.multiline" v-model:value="values[item]" type="text">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</mk-input>
<mk-textarea v-else-if="form[item].type === 'string' && item.multiline" v-model="values[item]">
</MkInput>
<MkTextarea v-else-if="form[item].type === 'string' && item.multiline" v-model:value="values[item]">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</mk-textarea>
<mk-switch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
</MkTextarea>
<MkSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</mk-switch>
</MkSwitch>
</label>
</div>
</x-window>
</XModalWindow>
</template>
<script lang="ts">
import Vue from 'vue';
import XWindow from './window.vue';
import { defineComponent } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkInput from './ui/input.vue';
import MkTextarea from './ui/textarea.vue';
import MkSwitch from './ui/switch.vue';
export default Vue.extend({
export default defineComponent({
components: {
XWindow,
XModalWindow,
MkInput,
MkTextarea,
MkSwitch,
@@ -52,6 +61,8 @@ export default Vue.extend({
},
},
emits: ['done'],
data() {
return {
values: {}
@@ -60,15 +71,24 @@ export default Vue.extend({
created() {
for (const item in this.form) {
Vue.set(this.values, item, this.form[item].hasOwnProperty('default') ? this.form[item].default : null);
this.values[item] = this.form[item].hasOwnProperty('default') ? this.form[item].default : null;
}
},
methods: {
ok() {
this.$emit('ok', this.values);
this.$refs.window.close();
this.$emit('done', {
result: this.values
});
this.$refs.dialog.close();
},
cancel() {
this.$emit('done', {
canceled: true
});
this.$refs.dialog.close();
}
}
});
</script>
@@ -77,7 +97,10 @@ export default Vue.extend({
.xkpnjxcv {
> label {
display: block;
padding: 16px 24px;
&:not(:last-child) {
margin-bottom: 32px;
}
}
}
</style>

View File

@@ -5,9 +5,10 @@
</template>
<script lang="ts">
import Vue from 'vue';
import * as katex from 'katex';
export default Vue.extend({
import { defineComponent } from 'vue';
import * as katex from 'katex';import * as os from '@/os';
export default defineComponent({
props: {
formula: {
type: String,

View File

@@ -1,12 +1,13 @@
<template>
<x-formula :formula="formula" :block="block" />
<XFormula :formula="formula" :block="block" />
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
import { defineComponent, defineAsyncComponent } from 'vue';import * as os from '@/os';
export default defineComponent({
components: {
XFormula: () => import('./formula-core.vue').then(m => m.default)
XFormula: defineAsyncComponent(() => import('./formula-core.vue'))
},
props: {
formula: {

View File

@@ -1,15 +1,16 @@
<template>
<div class="mk-google">
<input type="search" v-model="query" :placeholder="q">
<button @click="search"><fa :icon="faSearch"/> {{ $t('search') }}</button>
<button @click="search"><Fa :icon="faSearch"/> {{ $t('search') }}</button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faSearch } from '@fortawesome/free-solid-svg-icons';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: ['q'],
data() {
return {
@@ -23,7 +24,7 @@ export default Vue.extend({
methods: {
search() {
const engine = this.$store.state.settings.webSearchEngine ||
'https://www.google.com/?#q={{query}}';
'https://www.google.com/search?q={{query}}';
const url = engine.replace('{{query}}', this.query)
window.open(url, '_blank');
}

View File

@@ -8,16 +8,17 @@
</time>
</div>
<div class="content _panel _ghost">
<mk-clock/>
<MkClock/>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import MkClock from './analog-clock.vue';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
MkClock
},
@@ -48,7 +49,7 @@ export default Vue.extend({
this.tick();
this.clock = setInterval(this.tick, 1000);
},
beforeDestroy() {
beforeUnmount() {
clearInterval(this.clock);
},
methods: {

View File

@@ -0,0 +1,73 @@
<template>
<MkModal ref="modal" @click="type === 'success' ? done() : () => {}" @closed="$emit('closed')">
<div class="iuyakobc" :class="type">
<Fa class="icon" v-if="type === 'success'" :icon="faCheck"/>
<Fa class="icon" v-else-if="type === 'waiting'" :icon="faSpinner" pulse/>
</div>
</MkModal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faCheck, faSpinner } from '@fortawesome/free-solid-svg-icons';
import MkModal from '@/components/ui/modal.vue';
export default defineComponent({
components: {
MkModal,
},
props: {
type: {
required: true
},
showing: {
required: true
}
},
emits: ['done', 'closed'],
data() {
return {
faCheck, faSpinner,
};
},
watch: {
showing() {
if (!this.showing) this.done();
}
},
methods: {
done() {
this.$emit('done');
this.$refs.modal.close();
},
}
});
</script>
<style lang="scss" scoped>
.iuyakobc {
position: relative;
padding: 32px;
box-sizing: border-box;
text-align: center;
background: var(--panel);
border-radius: var(--radius);
width: initial;
font-size: 32px;
&.success {
color: var(--accent);
}
&.waiting {
> .icon {
opacity: 0.7;
}
}
}
</style>

View File

@@ -1,16 +1,26 @@
<template>
<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }">
<img class="xubzgfga" ref="img" :src="image.url" :alt="image.name" :title="image.name" @click="close" tabindex="-1"/>
</x-modal>
<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')">
<div class="xubzgfga">
<header>{{ image.name }}</header>
<img :src="image.url" :alt="image.name" :title="image.name" @click="$refs.modal.close()"/>
<footer>
<span>{{ image.type }}</span>
<span>{{ bytes(image.size) }}</span>
<span v-if="image.properties?.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
</footer>
</div>
</MkModal>
</template>
<script lang="ts">
import Vue from 'vue';
import XModal from './modal.vue';
import { defineComponent } from 'vue';
import bytes from '@/filters/bytes';
import number from '@/filters/number';
import MkModal from '@/components/ui/modal.vue';
export default Vue.extend({
export default defineComponent({
components: {
XModal,
MkModal,
},
props: {
@@ -20,32 +30,50 @@ export default Vue.extend({
},
},
mounted() {
this.$nextTick(() => {
this.$refs.img.focus();
});
},
emits: ['closed'],
methods: {
close() {
this.$refs.modal.close();
},
bytes,
number,
}
});
</script>
<style lang="scss" scoped>
.xubzgfga {
position: fixed;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
max-width: 100%;
max-height: 100%;
margin: auto;
cursor: zoom-out;
image-orientation: from-image;
max-width: 1024px;
> header,
> footer {
display: inline-block;
padding: 6px 9px;
font-size: 90%;
background: rgba(0, 0, 0, 0.5);
border-radius: 6px;
color: #fff;
}
> header {
margin-bottom: 8px;
opacity: 0.9;
}
> img {
display: block;
max-width: 100%;
cursor: zoom-out;
image-orientation: from-image;
}
> footer {
margin-top: 8px;
opacity: 0.8;
> span + span {
margin-left: 0.5em;
padding-left: 0.5em;
border-left: solid 1px rgba(255, 255, 255, 0.5);
}
}
}
</style>

View File

@@ -1,15 +1,15 @@
<template>
<div class="xubzgfgb" :title="title">
<div class="xubzgfgb" :class="{ cover }" :title="title">
<canvas ref="canvas" :width="size" :height="size" :title="title" v-if="!loaded"/>
<img v-if="src" :src="src" :title="title" :alt="alt" @load="onLoad"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { decode } from 'blurhash';
export default Vue.extend({
export default defineComponent({
props: {
src: {
type: String,
@@ -35,6 +35,11 @@ export default Vue.extend({
required: false,
default: 64
},
cover: {
type: Boolean,
required: false,
default: true,
}
},
data() {
@@ -49,6 +54,7 @@ export default Vue.extend({
methods: {
draw() {
if (this.hash == null) return;
const pixels = decode(this.hash, this.size, this.size);
const ctx = (this.$refs.canvas as HTMLCanvasElement).getContext('2d');
const imageData = ctx!.createImageData(this.size, this.size);
@@ -70,9 +76,23 @@ export default Vue.extend({
> canvas,
> img {
display: block;
width: 100%;
height: 100%;
}
> canvas {
object-fit: cover;
}
> img {
object-fit: contain;
}
&.cover {
> img {
object-fit: cover;
}
}
}
</style>

View File

@@ -1,4 +1,4 @@
import Vue from 'vue';
import { App } from 'vue';
import mfm from './misskey-flavored-markdown.vue';
import acct from './acct.vue';
@@ -12,14 +12,16 @@ import loading from './loading.vue';
import error from './error.vue';
import streamIndicator from './stream-indicator.vue';
Vue.component('mfm', mfm);
Vue.component('mk-acct', acct);
Vue.component('mk-avatar', avatar);
Vue.component('mk-emoji', emoji);
Vue.component('mk-user-name', userName);
Vue.component('mk-ellipsis', ellipsis);
Vue.component('mk-time', time);
Vue.component('mk-url', url);
Vue.component('mk-loading', loading);
Vue.component('mk-error', error);
Vue.component('stream-indicator', streamIndicator);
export default function(app: App) {
app.component('Mfm', mfm);
app.component('MkAcct', acct);
app.component('MkAvatar', avatar);
app.component('MkEmoji', emoji);
app.component('MkUserName', userName);
app.component('MkEllipsis', ellipsis);
app.component('MkTime', time);
app.component('MkUrl', url);
app.component('MkLoading', loading);
app.component('MkError', error);
app.component('StreamIndicator', streamIndicator);
}

View File

@@ -1,93 +1,93 @@
<template>
<div class="zbcjwnqg">
<div class="zbcjwnqg" v-size="{ max: [550, 1000] }">
<div class="stats" v-if="info">
<div class="_panel">
<div>
<b><fa :icon="faUser"/>{{ $t('users') }}</b>
<b><Fa :icon="faUser"/>{{ $t('users') }}</b>
<small>{{ $t('local') }}</small>
</div>
<div>
<dl class="total">
<dt>{{ $t('total') }}</dt>
<dd>{{ info.originalUsersCount | number }}</dd>
<dd>{{ number(info.originalUsersCount) }}</dd>
</dl>
<dl class="diff" :class="{ inc: usersLocalDoD > 0 }">
<dt>{{ $t('dayOverDayChanges') }}</dt>
<dd>{{ usersLocalDoD | number }}</dd>
<dd>{{ number(usersLocalDoD) }}</dd>
</dl>
<dl class="diff" :class="{ inc: usersLocalWoW > 0 }">
<dt>{{ $t('weekOverWeekChanges') }}</dt>
<dd>{{ usersLocalWoW | number }}</dd>
<dd>{{ number(usersLocalWoW) }}</dd>
</dl>
</div>
</div>
<div class="_panel">
<div>
<b><fa :icon="faUser"/>{{ $t('users') }}</b>
<b><Fa :icon="faUser"/>{{ $t('users') }}</b>
<small>{{ $t('remote') }}</small>
</div>
<div>
<dl class="total">
<dt>{{ $t('total') }}</dt>
<dd>{{ (info.usersCount - info.originalUsersCount) | number }}</dd>
<dd>{{ number((info.usersCount - info.originalUsersCount)) }}</dd>
</dl>
<dl class="diff" :class="{ inc: usersRemoteDoD > 0 }">
<dt>{{ $t('dayOverDayChanges') }}</dt>
<dd>{{ usersRemoteDoD | number }}</dd>
<dd>{{ number(usersRemoteDoD) }}</dd>
</dl>
<dl class="diff" :class="{ inc: usersRemoteWoW > 0 }">
<dt>{{ $t('weekOverWeekChanges') }}</dt>
<dd>{{ usersRemoteWoW | number }}</dd>
<dd>{{ number(usersRemoteWoW) }}</dd>
</dl>
</div>
</div>
<div class="_panel">
<div>
<b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
<b><Fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
<small>{{ $t('local') }}</small>
</div>
<div>
<dl class="total">
<dt>{{ $t('total') }}</dt>
<dd>{{ info.originalNotesCount | number }}</dd>
<dd>{{ number(info.originalNotesCount) }}</dd>
</dl>
<dl class="diff" :class="{ inc: notesLocalDoD > 0 }">
<dt>{{ $t('dayOverDayChanges') }}</dt>
<dd>{{ notesLocalDoD | number }}</dd>
<dd>{{ number(notesLocalDoD) }}</dd>
</dl>
<dl class="diff" :class="{ inc: notesLocalWoW > 0 }">
<dt>{{ $t('weekOverWeekChanges') }}</dt>
<dd>{{ notesLocalWoW | number }}</dd>
<dd>{{ number(notesLocalWoW) }}</dd>
</dl>
</div>
</div>
<div class="_panel">
<div>
<b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
<b><Fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
<small>{{ $t('remote') }}</small>
</div>
<div>
<dl class="total">
<dt>{{ $t('total') }}</dt>
<dd>{{ (info.notesCount - info.originalNotesCount) | number }}</dd>
<dd>{{ number((info.notesCount - info.originalNotesCount)) }}</dd>
</dl>
<dl class="diff" :class="{ inc: notesRemoteDoD > 0 }">
<dt>{{ $t('dayOverDayChanges') }}</dt>
<dd>{{ notesRemoteDoD | number }}</dd>
<dd>{{ number(notesRemoteDoD) }}</dd>
</dl>
<dl class="diff" :class="{ inc: notesRemoteWoW > 0 }">
<dt>{{ $t('weekOverWeekChanges') }}</dt>
<dd>{{ notesRemoteWoW | number }}</dd>
<dd>{{ number(notesRemoteWoW) }}</dd>
</dl>
</div>
</div>
</div>
<section class="_card">
<div class="_title"><fa :icon="faChartBar"/> {{ $t('statistics') }}</div>
<div class="_title" style="position: relative;"><Fa :icon="faChartBar"/> {{ $t('statistics') }}<button @click="fetchChart" class="_button" style="position: absolute; right: 0; bottom: 0; top: 0; padding: inherit;"><Fa :icon="faSync"/></button></div>
<div class="_content" style="margin-top: -8px;">
<div class="selects" style="display: flex;">
<mk-select v-model="chartSrc" style="margin: 0; flex: 1;">
<MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;">
<optgroup :label="$t('federation')">
<option value="federation-instances">{{ $t('_charts.federationInstancesIncDec') }}</option>
<option value="federation-instances-total">{{ $t('_charts.federationInstancesTotal') }}</option>
@@ -109,11 +109,11 @@
<option value="drive">{{ $t('_charts.storageUsageIncDec') }}</option>
<option value="drive-total">{{ $t('_charts.storageUsageTotal') }}</option>
</optgroup>
</mk-select>
<mk-select v-model="chartSpan" style="margin: 0;">
</MkSelect>
<MkSelect v-model:value="chartSpan" style="margin: 0;">
<option value="hour">{{ $t('perHour') }}</option>
<option value="day">{{ $t('perDay') }}</option>
</mk-select>
</MkSelect>
</div>
<canvas ref="chart"></canvas>
</div>
@@ -122,12 +122,12 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { faChartBar, faUser, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { defineComponent, markRaw } from 'vue';
import { faChartBar, faUser, faPencilAlt, faSync } from '@fortawesome/free-solid-svg-icons';
import Chart from 'chart.js';
import MkSelect from './ui/select.vue';
import number from '@/filters/number';
const chartLimit = 90;
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
const negate = arr => arr.map(x => -x);
const alpha = (hex, a) => {
@@ -137,12 +137,26 @@ const alpha = (hex, a) => {
const b = parseInt(result[3], 16);
return `rgba(${r}, ${g}, ${b}, ${a})`;
};
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
MkSelect
},
props: {
chartLimit: {
type: Number,
required: false,
default: 90
},
detailed: {
type: Boolean,
required: false,
default: false
},
},
data() {
return {
info: null,
@@ -159,7 +173,7 @@ export default Vue.extend({
chartInstance: null,
chartSrc: 'notes',
chartSpan: 'hour',
faChartBar, faUser, faPencilAlt
faChartBar, faUser, faPencilAlt, faSync
}
},
@@ -204,66 +218,73 @@ export default Vue.extend({
},
async created() {
this.info = await this.$root.api('stats');
this.info = await os.api('stats');
this.now = new Date();
const [perHour, perDay] = await Promise.all([Promise.all([
this.$root.api('charts/federation', { limit: chartLimit, span: 'hour' }),
this.$root.api('charts/users', { limit: chartLimit, span: 'hour' }),
this.$root.api('charts/active-users', { limit: chartLimit, span: 'hour' }),
this.$root.api('charts/notes', { limit: chartLimit, span: 'hour' }),
this.$root.api('charts/drive', { limit: chartLimit, span: 'hour' }),
]), Promise.all([
this.$root.api('charts/federation', { limit: chartLimit, span: 'day' }),
this.$root.api('charts/users', { limit: chartLimit, span: 'day' }),
this.$root.api('charts/active-users', { limit: chartLimit, span: 'day' }),
this.$root.api('charts/notes', { limit: chartLimit, span: 'day' }),
this.$root.api('charts/drive', { limit: chartLimit, span: 'day' }),
])]);
const chart = {
perHour: {
federation: perHour[0],
users: perHour[1],
activeUsers: perHour[2],
notes: perHour[3],
drive: perHour[4],
},
perDay: {
federation: perDay[0],
users: perDay[1],
activeUsers: perDay[2],
notes: perDay[3],
drive: perDay[4],
}
};
this.notesLocalWoW = this.info.originalNotesCount - chart.perDay.notes.local.total[7];
this.notesLocalDoD = this.info.originalNotesCount - chart.perDay.notes.local.total[1];
this.notesRemoteWoW = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[7];
this.notesRemoteDoD = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[1];
this.usersLocalWoW = this.info.originalUsersCount - chart.perDay.users.local.total[7];
this.usersLocalDoD = this.info.originalUsersCount - chart.perDay.users.local.total[1];
this.usersRemoteWoW = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[7];
this.usersRemoteDoD = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[1];
this.chart = chart;
this.renderChart();
this.fetchChart();
},
methods: {
async fetchChart() {
const [perHour, perDay] = await Promise.all([Promise.all([
os.api('charts/federation', { limit: this.chartLimit, span: 'hour' }),
os.api('charts/users', { limit: this.chartLimit, span: 'hour' }),
os.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }),
os.api('charts/notes', { limit: this.chartLimit, span: 'hour' }),
os.api('charts/drive', { limit: this.chartLimit, span: 'hour' }),
]), Promise.all([
os.api('charts/federation', { limit: this.chartLimit, span: 'day' }),
os.api('charts/users', { limit: this.chartLimit, span: 'day' }),
os.api('charts/active-users', { limit: this.chartLimit, span: 'day' }),
os.api('charts/notes', { limit: this.chartLimit, span: 'day' }),
os.api('charts/drive', { limit: this.chartLimit, span: 'day' }),
])]);
const chart = {
perHour: {
federation: perHour[0],
users: perHour[1],
activeUsers: perHour[2],
notes: perHour[3],
drive: perHour[4],
},
perDay: {
federation: perDay[0],
users: perDay[1],
activeUsers: perDay[2],
notes: perDay[3],
drive: perDay[4],
}
};
this.notesLocalWoW = this.info.originalNotesCount - chart.perDay.notes.local.total[7];
this.notesLocalDoD = this.info.originalNotesCount - chart.perDay.notes.local.total[1];
this.notesRemoteWoW = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[7];
this.notesRemoteDoD = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[1];
this.usersLocalWoW = this.info.originalUsersCount - chart.perDay.users.local.total[7];
this.usersLocalDoD = this.info.originalUsersCount - chart.perDay.users.local.total[1];
this.usersRemoteWoW = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[7];
this.usersRemoteDoD = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[1];
this.chart = chart;
this.renderChart();
},
renderChart() {
if (this.chartInstance) {
this.chartInstance.destroy();
}
// TODO: var(--panel)の色が暗いか明るいかで判定する
const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
this.chartInstance = new Chart(this.$refs.chart, {
this.chartInstance = markRaw(new Chart(this.$refs.chart, {
type: 'line',
data: {
labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
datasets: this.data.series.map(x => ({
label: x.name,
data: x.data.slice().reverse(),
@@ -271,7 +292,9 @@ export default Vue.extend({
lineTension: 0,
borderWidth: 2,
borderColor: x.color,
borderDash: x.borderDash || [],
backgroundColor: alpha(x.color, 0.1),
fill: x.fill == null ? true : x.fill,
hidden: !!x.hidden
}))
},
@@ -293,17 +316,28 @@ export default Vue.extend({
},
scales: {
xAxes: [{
type: 'time',
time: {
stepSize: 1,
unit: this.chartSpan == 'day' ? 'month' : 'day',
},
gridLines: {
display: false
display: this.detailed,
color: gridColor,
zeroLineColor: gridColor,
},
ticks: {
display: false
display: this.detailed
}
}],
yAxes: [{
position: 'right',
position: 'left',
gridLines: {
color: gridColor,
zeroLineColor: gridColor,
},
ticks: {
display: false
display: this.detailed
}
}]
},
@@ -312,7 +346,7 @@ export default Vue.extend({
mode: 'index',
}
}
});
}));
},
getDate(ago: number) {
@@ -325,7 +359,11 @@ export default Vue.extend({
},
format(arr) {
return arr;
const now = Date.now();
return arr.map((v, i) => ({
x: new Date(now - ((this.chartSpan == 'day' ? 86400000 :3600000 ) * i)),
y: v
}));
},
federationInstancesChart(total: boolean): any {
@@ -347,6 +385,8 @@ export default Vue.extend({
name: 'All',
type: 'line',
color: '#008FFB',
borderDash: [5, 5],
fill: false,
data: this.format(type == 'combined'
? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec))
: sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec))
@@ -463,7 +503,9 @@ export default Vue.extend({
series: [{
name: 'All',
type: 'line',
color: '#008FFB',
color: '#09d8e2',
borderDash: [5, 5],
fill: false,
data: this.format(
sum(
this.stats.drive.local.incSize,
@@ -480,17 +522,17 @@ export default Vue.extend({
}, {
name: 'Local -',
type: 'area',
color: '#008FFB',
color: '#FF4560',
data: this.format(negate(this.stats.drive.local.decSize))
}, {
name: 'Remote +',
type: 'area',
color: '#008FFB',
color: '#00E396',
data: this.format(this.stats.drive.remote.incSize)
}, {
name: 'Remote -',
type: 'area',
color: '#008FFB',
color: '#FEB019',
data: this.format(negate(this.stats.drive.remote.decSize))
}]
};
@@ -525,7 +567,9 @@ export default Vue.extend({
series: [{
name: 'All',
type: 'line',
color: '#008FFB',
color: '#09d8e2',
borderDash: [5, 5],
fill: false,
data: this.format(
sum(
this.stats.drive.local.incCount,
@@ -542,17 +586,17 @@ export default Vue.extend({
}, {
name: 'Local -',
type: 'area',
color: '#008FFB',
color: '#FF4560',
data: this.format(negate(this.stats.drive.local.decCount))
}, {
name: 'Remote +',
type: 'area',
color: '#008FFB',
color: '#00E396',
data: this.format(this.stats.drive.remote.incCount)
}, {
name: 'Remote -',
type: 'area',
color: '#008FFB',
color: '#FEB019',
data: this.format(negate(this.stats.drive.remote.decCount))
}]
};
@@ -580,23 +624,38 @@ export default Vue.extend({
}]
};
},
number
}
});
</script>
<style lang="scss" scoped>
.zbcjwnqg {
&.max-width_1000px {
> .stats {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
}
&.max-width_550px {
> .stats {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr 1fr 1fr;
}
}
> .stats {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin: calc(0px - var(--margin) / 2);
margin-bottom: calc(var(--margin) / 2);
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-template-rows: 1fr;
gap: var(--margin);
margin-bottom: var(--margin);
font-size: 90%;
> div {
display: flex;
flex: 1 0 213px;
margin: calc(var(--margin) / 2);
box-sizing: border-box;
padding: 16px 20px;
@@ -631,7 +690,7 @@ export default Vue.extend({
margin: 0;
}
> dt {
> dd {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;

View File

@@ -5,18 +5,18 @@
:title="url"
>
<slot></slot>
<fa :icon="faExternalLinkSquareAlt" v-if="target === '_blank'" class="icon"/>
<Fa :icon="faExternalLinkSquareAlt" v-if="target === '_blank'" class="icon"/>
</component>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
import { url as local } from '../config';
import MkUrlPreview from './url-preview-popup.vue';
import { isDeviceTouch } from '../scripts/is-device-touch';
import { url as local } from '@/config';
import { isDeviceTouch } from '@/scripts/is-device-touch';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
url: {
type: String,
@@ -36,29 +36,34 @@ export default Vue.extend({
target: self ? null : '_blank',
showTimer: null,
hideTimer: null,
preview: null,
checkTimer: null,
close: null,
faExternalLinkSquareAlt
};
},
methods: {
showPreview() {
async showPreview() {
if (!document.body.contains(this.$el)) return;
if (this.preview) return;
if (this.close) return;
this.preview = new MkUrlPreview({
parent: this,
propsData: {
url: this.url,
source: this.$el
}
}).$mount();
const { dispose } = os.popup(await import('@/components/url-preview-popup.vue'), {
url: this.url,
source: this.$el
});
document.body.appendChild(this.preview.$el);
this.close = () => {
dispose();
};
this.checkTimer = setInterval(() => {
if (!document.body.contains(this.$el)) this.closePreview();
}, 1000);
},
closePreview() {
if (this.preview) {
this.preview.destroyDom();
this.preview = null;
if (this.close) {
clearInterval(this.checkTimer);
this.close();
this.close = null;
}
},
onMouseover() {

View File

@@ -5,9 +5,10 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
inline: {
type: Boolean,

View File

@@ -1,7 +1,7 @@
<template>
<div class="mk-media-banner">
<div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false">
<span class="icon"><fa :icon="faExclamationTriangle"/></span>
<span class="icon"><Fa :icon="faExclamationTriangle"/></span>
<b>{{ $t('sensitive') }}</b>
<span>{{ $t('clickToShow') }}</span>
</div>
@@ -19,17 +19,18 @@
:title="media.name"
:download="media.name"
>
<span class="icon"><fa icon="download"/></span>
<span class="icon"><Fa icon="download"/></span>
<b>{{ media.name }}</b>
</a>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
media: {
type: Object,

View File

@@ -1,34 +1,36 @@
<template>
<div class="qjewsnkg" v-if="hide" @click="hide = false">
<img-with-blurhash class="bg" :hash="image.blurhash" :title="image.name"/>
<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.name"/>
<div class="text">
<div>
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
<b><Fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
<span>{{ $t('clickToShow') }}</span>
</div>
</div>
</div>
<div class="gqnyydlz" v-else>
<i><fa :icon="faEyeSlash" @click="hide = true"/></i>
<div class="gqnyydlz" :style="{ background: color }" v-else>
<i><Fa :icon="faEyeSlash" @click="hide = true"/></i>
<a
:href="image.url"
:title="image.name"
@click.prevent="onClick"
>
<img-with-blurhash :hash="image.blurhash" :src="url" :alt="image.name" :title="image.name"/>
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.name" :title="image.name" :cover="false"/>
<div class="gif" v-if="image.type === 'image/gif'">GIF</div>
</a>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
import { getStaticImageUrl } from '../scripts/get-static-image-url';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
import ImageViewer from './image-viewer.vue';
import ImgWithBlurhash from './img-with-blurhash.vue';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
ImgWithBlurhash
},
@@ -44,8 +46,8 @@ export default Vue.extend({
data() {
return {
hide: true,
faExclamationTriangle,
faEyeSlash
color: null,
faExclamationTriangle, faEyeSlash,
};
},
computed: {
@@ -64,19 +66,25 @@ export default Vue.extend({
}
},
created() {
this.hide = this.image.isSensitive && !this.$store.state.device.alwaysShowNsfw;
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
this.$watch('image', () => {
this.hide = this.image.isSensitive && !this.$store.state.device.alwaysShowNsfw;
if (this.image.blurhash) {
this.color = extractAvgColorFromBlurhash(this.image.blurhash);
}
}, {
deep: true,
immediate: true,
});
},
methods: {
onClick() {
if (this.$store.state.device.imageNewTab) {
window.open(this.image.url, '_blank');
} else {
const viewer = this.$root.new(ImageViewer, {
os.popup(ImageViewer, {
image: this.image
});
this.$once('hook:beforeDestroy', () => {
viewer.close();
});
}, {}, 'closed');
}
}
}
@@ -117,6 +125,7 @@ export default Vue.extend({
.gqnyydlz {
position: relative;
border: solid 1px var(--divider);
> i {
display: block;

View File

@@ -1,13 +1,11 @@
<template>
<div class="mk-media-list">
<template v-for="media in mediaList.filter(media => !previewable(media))">
<x-banner :media="media" :key="media.id"/>
</template>
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :media="media" :key="media.id"/>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container" ref="gridOuter">
<div :data-count="mediaList.filter(media => previewable(media)).length" :style="gridInnerStyle">
<template v-for="media in mediaList">
<x-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
<x-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
<XVideo :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
<XImage :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
</template>
</div>
</div>
@@ -15,12 +13,13 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import XBanner from './media-banner.vue';
import XImage from './media-image.vue';
import XVideo from './media-video.vue';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
XBanner,
XImage,
@@ -46,7 +45,7 @@ export default Vue.extend({
this.size();
window.addEventListener('resize', this.size);
},
beforeDestroy() {
beforeUnmount() {
window.removeEventListener('resize', this.size);
},
activated() {

View File

@@ -1,12 +1,12 @@
<template>
<div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="hide" @click="hide = false">
<div>
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
<b><Fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
<span>{{ $t('clickToShow') }}</span>
</div>
</div>
<div class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else>
<i><fa :icon="faEyeSlash" @click="hide = true"/></i>
<i><Fa :icon="faEyeSlash" @click="hide = true"/></i>
<a
:href="video.url"
rel="nofollow noopener"
@@ -14,17 +14,18 @@
:style="imageStyle"
:title="video.name"
>
<fa :icon="faPlayCircle"/>
<Fa :icon="faPlayCircle"/>
</a>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faPlayCircle } from '@fortawesome/free-regular-svg-icons';
import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
video: {
type: Object,

View File

@@ -15,11 +15,13 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { toUnicode } from 'punycode';
import { host as localHost } from '../config';
import { host as localHost } from '@/config';
import { wellKnownServices } from '../../well-known-services';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
username: {
type: String,
@@ -37,12 +39,11 @@ export default Vue.extend({
},
computed: {
url(): string {
switch (this.host) {
case 'twitter.com':
case 'github.com':
return `https://${this.host}/${this.username}`;
default:
return `/${this.canonical}`;
const wellKnown = wellKnownServices.find(x => x[0] === this.host);
if (wellKnown) {
return wellKnown[1](this.username);
} else {
return `/${this.canonical}`;
}
},
canonical(): string {

View File

@@ -1,191 +0,0 @@
<template>
<x-popup :source="source" :no-center="noCenter" :fixed="fixed" :width="width" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap">
<div class="rrevdjwt" :class="{ left: align === 'left' }" ref="items">
<template v-for="(item, i) in items.filter(item => item !== undefined)">
<div v-if="item === null" class="divider" :key="i"></div>
<span v-else-if="item.type === 'label'" class="label item" :key="i">
<span>{{ item.text }}</span>
</span>
<router-link v-else-if="item.type === 'link'" :to="item.to" @click.native="close()" :tabindex="i" class="_button item" :key="i">
<fa v-if="item.icon" :icon="item.icon" fixed-width/>
<mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span>
<i v-if="item.indicate"><fa :icon="faCircle"/></i>
</router-link>
<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" @click="close()" :tabindex="i" class="_button item" :key="i">
<fa v-if="item.icon" :icon="item.icon" fixed-width/>
<span>{{ item.text }}</span>
<i v-if="item.indicate"><fa :icon="faCircle"/></i>
</a>
<button v-else-if="item.type === 'user'" @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i">
<mk-avatar :user="item.user" class="avatar"/><mk-user-name :user="item.user"/>
<i v-if="item.indicate"><fa :icon="faCircle"/></i>
</button>
<button v-else @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i">
<fa v-if="item.icon" :icon="item.icon" fixed-width/>
<mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span>
<i v-if="item.indicate"><fa :icon="faCircle"/></i>
</button>
</template>
</div>
</x-popup>
</template>
<script lang="ts">
import Vue from 'vue';
import { faCircle } from '@fortawesome/free-solid-svg-icons';
import XPopup from './popup.vue';
import { focusPrev, focusNext } from '../scripts/focus';
export default Vue.extend({
components: {
XPopup
},
props: {
source: {
required: true
},
items: {
type: Array,
required: true
},
align: {
type: String,
required: false
},
noCenter: {
type: Boolean,
required: false
},
fixed: {
type: Boolean,
required: false
},
width: {
type: Number,
required: false
},
direction: {
type: String,
required: false
},
viaKeyboard: {
type: Boolean,
required: false
},
},
data() {
return {
faCircle
};
},
computed: {
keymap(): any {
return {
'up|k|shift+tab': this.focusUp,
'down|j|tab': this.focusDown,
};
},
},
mounted() {
if (this.viaKeyboard) {
this.$nextTick(() => {
focusNext(this.$refs.items.children[0], true);
});
}
},
methods: {
clicked(fn) {
fn();
this.close();
},
close() {
this.$refs.popup.close();
},
focusUp() {
focusPrev(document.activeElement);
},
focusDown() {
focusNext(document.activeElement);
}
}
});
</script>
<style lang="scss" scoped>
.rrevdjwt {
padding: 8px 0;
&.left {
> .item {
text-align: left;
}
}
> .item {
display: block;
position: relative;
padding: 8px 16px;
width: 100%;
box-sizing: border-box;
white-space: nowrap;
font-size: 0.9em;
line-height: 20px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
color: #fff;
background: var(--accent);
text-decoration: none;
}
&:active {
color: #fff;
background: var(--accentDarken);
}
&:not(:active):focus {
box-shadow: 0 0 0 2px var(--focus) inset;
}
&.label {
pointer-events: none;
font-size: 0.7em;
padding-bottom: 4px;
> span {
opacity: 0.7;
}
}
> [data-icon] {
margin-right: 4px;
width: 20px;
}
> .avatar {
margin-right: 4px;
width: 20px;
height: 20px;
}
> i {
position: absolute;
top: 5px;
left: 13px;
color: var(--indicator);
font-size: 12px;
animation: blink 1s infinite;
}
}
> .divider {
margin: 8px 0;
height: 1px;
background: var(--divider);
}
}
</style>

View File

@@ -1,16 +1,18 @@
import Vue, { VNode } from 'vue';
import { VNode, defineComponent, h } from 'vue';
import { MfmForest } from '../../mfm/prelude';
import { parse, parsePlain } from '../../mfm/parse';
import MkUrl from './url.vue';
import MkLink from './link.vue';
import MkMention from './mention.vue';
import MkEmoji from './emoji.vue';
import { concat } from '../../prelude/array';
import MkFormula from './formula.vue';
import MkCode from './code.vue';
import MkGoogle from './google.vue';
import { host } from '../config';
import { host } from '@/config';
import { RouterLink } from 'vue-router';
export default Vue.component('misskey-flavored-markdown', {
export default defineComponent({
props: {
text: {
type: String,
@@ -41,7 +43,7 @@ export default Vue.component('misskey-flavored-markdown', {
},
},
render(createElement) {
render() {
if (this.text == null || this.text == '') return;
const ast = (this.plain ? parsePlain : parse)(this.text);
@@ -53,67 +55,49 @@ export default Vue.component('misskey-flavored-markdown', {
if (!this.plain) {
const x = text.split('\n')
.map(t => t == '' ? [createElement('br')] : [this._v(t), createElement('br')]); // NOTE: this._vはHACK SEE: https://github.com/syuilo/misskey/pull/6399#issuecomment-632820283
.map(t => t == '' ? [h('br')] : [t, h('br')]);
x[x.length - 1].pop();
return x;
} else {
return [this._v(text.replace(/\n/g, ' '))];
return [text.replace(/\n/g, ' ')];
}
}
case 'bold': {
return [createElement('b', genEl(token.children))];
return [h('b', genEl(token.children))];
}
case 'strike': {
return [createElement('del', genEl(token.children))];
return [h('del', genEl(token.children))];
}
case 'italic': {
return (createElement as any)('i', {
attrs: {
style: 'font-style: oblique;'
},
return h('i', {
style: 'font-style: oblique;'
}, genEl(token.children));
}
case 'big': {
return (createElement as any)('strong', {
attrs: {
style: `display: inline-block; font-size: 150%;`
},
directives: [this.$store.state.device.animatedMfm ? {
name: 'animate-css',
value: { classes: 'tada', iteration: 'infinite' }
}: {}]
return h('strong', {
style: `display: inline-block; font-size: 150%;` + (this.$store.state.device.animatedMfm ? 'animation: anime-tada 1s linear infinite both;' : ''),
}, genEl(token.children));
}
case 'small': {
return [createElement('small', {
attrs: {
style: 'opacity: 0.7;'
},
return [h('small', {
style: 'opacity: 0.7;'
}, genEl(token.children))];
}
case 'center': {
return [createElement('div', {
attrs: {
style: 'text-align:center;'
}
return [h('div', {
style: 'text-align:center;'
}, genEl(token.children))];
}
case 'motion': {
return (createElement as any)('span', {
attrs: {
style: 'display: inline-block;'
},
directives: [this.$store.state.device.animatedMfm ? {
name: 'animate-css',
value: { classes: 'rubberBand', iteration: 'infinite' }
} : {}]
return h('span', {
style: 'display: inline-block;' + (this.$store.state.device.animatedMfm ? 'animation: anime-rubberBand 1s linear infinite both;' : ''),
}, genEl(token.children));
}
@@ -123,163 +107,126 @@ export default Vue.component('misskey-flavored-markdown', {
token.node.props.attr == 'alternate' ? 'alternate' :
'normal';
const style = this.$store.state.device.animatedMfm
? `animation: spin 1.5s linear infinite; animation-direction: ${direction};` : '';
return (createElement as any)('span', {
attrs: {
style: 'display: inline-block;' + style
},
? `animation: anime-spin 1.5s linear infinite; animation-direction: ${direction};` : '';
return h('span', {
style: 'display: inline-block;' + style
}, genEl(token.children));
}
case 'jump': {
return (createElement as any)('span', {
attrs: {
style: this.$store.state.device.animatedMfm ? 'display: inline-block; animation: jump 0.75s linear infinite;' : 'display: inline-block;'
},
return h('span', {
style: this.$store.state.device.animatedMfm ? 'display: inline-block; animation: anime-jump 0.75s linear infinite;' : 'display: inline-block;'
}, genEl(token.children));
}
case 'flip': {
return (createElement as any)('span', {
attrs: {
style: 'display: inline-block; transform: scaleX(-1);'
},
return h('span', {
style: 'display: inline-block; transform: scaleX(-1);'
}, genEl(token.children));
}
case 'url': {
return [createElement(MkUrl, {
return [h(MkUrl, {
key: Math.random(),
props: {
url: token.node.props.url,
rel: 'nofollow noopener',
},
url: token.node.props.url,
rel: 'nofollow noopener',
})];
}
case 'link': {
return [createElement(MkLink, {
return [h(MkLink, {
key: Math.random(),
props: {
url: token.node.props.url,
rel: 'nofollow noopener',
},
url: token.node.props.url,
rel: 'nofollow noopener',
}, genEl(token.children))];
}
case 'mention': {
return [createElement(MkMention, {
return [h(MkMention, {
key: Math.random(),
props: {
host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host,
username: token.node.props.username
}
host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host,
username: token.node.props.username
})];
}
case 'hashtag': {
return [createElement('router-link', {
return [h(RouterLink, {
key: Math.random(),
attrs: {
to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`,
style: 'color:var(--hashtag);'
}
to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`,
style: 'color:var(--hashtag);'
}, `#${token.node.props.hashtag}`)];
}
case 'blockCode': {
return [createElement(MkCode, {
return [h(MkCode, {
key: Math.random(),
props: {
code: token.node.props.code,
lang: token.node.props.lang,
}
code: token.node.props.code,
lang: token.node.props.lang,
})];
}
case 'inlineCode': {
return [createElement(MkCode, {
return [h(MkCode, {
key: Math.random(),
props: {
code: token.node.props.code,
lang: token.node.props.lang,
inline: true
}
code: token.node.props.code,
lang: token.node.props.lang,
inline: true
})];
}
case 'quote': {
if (this.shouldBreak) {
return [createElement('div', {
attrs: {
class: 'quote'
}
if (!this.nowrap) {
return [h('div', {
class: 'quote'
}, genEl(token.children))];
} else {
return [createElement('span', {
attrs: {
class: 'quote'
}
return [h('span', {
class: 'quote'
}, genEl(token.children))];
}
}
case 'title': {
return [createElement('div', {
attrs: {
class: 'title'
}
return [h('div', {
class: 'title'
}, genEl(token.children))];
}
case 'emoji': {
return [createElement('mk-emoji', {
return [h(MkEmoji, {
key: Math.random(),
attrs: {
emoji: token.node.props.emoji,
name: token.node.props.name
},
props: {
customEmojis: this.customEmojis,
normal: this.plain
}
emoji: token.node.props.emoji,
name: token.node.props.name,
customEmojis: this.customEmojis,
normal: this.plain
})];
}
case 'mathInline': {
//const MkFormula = () => import('./formula.vue').then(m => m.default);
return [createElement(MkFormula, {
return [h(MkFormula, {
key: Math.random(),
props: {
formula: token.node.props.formula,
block: false
}
formula: token.node.props.formula,
block: false
})];
}
case 'mathBlock': {
//const MkFormula = () => import('./formula.vue').then(m => m.default);
return [createElement(MkFormula, {
return [h(MkFormula, {
key: Math.random(),
props: {
formula: token.node.props.formula,
block: true
}
formula: token.node.props.formula,
block: true
})];
}
case 'search': {
//const MkGoogle = () => import('./google.vue').then(m => m.default);
return [createElement(MkGoogle, {
return [h(MkGoogle, {
key: Math.random(),
props: {
q: token.node.props.query
}
q: token.node.props.query
})];
}
default: {
console.log('unknown ast type:', token.node.type);
console.error('unrecognized ast type:', token.node.type);
return [];
}
@@ -287,6 +234,6 @@ export default Vue.component('misskey-flavored-markdown', {
}));
// Parse ast to DOM
return createElement('span', genEl(ast));
return h('span', genEl(ast));
}
});

View File

@@ -30,10 +30,11 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { v4 as uuid } from 'uuid';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
src: {
type: Array,
@@ -64,7 +65,7 @@ export default Vue.extend({
// Vueが何故かWatchを発動させない場合があるので
this.clock = setInterval(this.draw, 1000);
},
beforeDestroy() {
beforeUnmount() {
clearInterval(this.clock);
},
methods: {

View File

@@ -3,10 +3,10 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import MfmCore from './mfm';
export default Vue.extend({
export default defineComponent({
components: {
MfmCore
}
@@ -24,7 +24,7 @@ export default Vue.extend({
text-overflow: ellipsis;
}
::v-deep .quote {
::v-deep(.quote) {
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
@@ -33,15 +33,15 @@ export default Vue.extend({
opacity: 0.7;
}
::v-deep pre {
::v-deep(pre) {
font-size: 0.8em;
}
::v-deep > code {
> ::v-deep(code) {
word-break: break-all;
}
::v-deep .title {
::v-deep(.title) {
text-align: center;
border-bottom: solid 1px var(--divider);
}

View File

@@ -1,90 +0,0 @@
<template>
<div class="mk-modal" v-hotkey.global="keymap">
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
<div class="bg _modalBg" ref="bg" v-if="show" @click="canClose ? close() : () => {}"></div>
</transition>
<transition :name="$store.state.device.animation ? 'modal' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
<div class="content" ref="content" v-if="show" @click.self="canClose ? close() : () => {}"><slot></slot></div>
</transition>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
canClose: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
show: true,
};
},
computed: {
keymap(): any {
return {
'esc': this.close,
};
},
},
methods: {
close() {
this.show = false;
(this.$refs.bg as any).style.pointerEvents = 'none';
(this.$refs.content as any).style.pointerEvents = 'none';
}
}
});
</script>
<style lang="scss" scoped>
.modal-enter-active, .modal-leave-active {
transition: opacity 0.3s, transform 0.3s !important;
}
.modal-enter, .modal-leave-to {
opacity: 0;
transform: scale(0.9);
}
.bg-fade-enter-active, .bg-fade-leave-active {
transition: opacity 0.3s !important;
}
.bg-fade-enter, .bg-fade-leave-to {
opacity: 0;
}
.mk-modal {
> .bg {
z-index: 10000;
}
> .content {
position: fixed;
z-index: 10000;
top: 0;
bottom: 0;
left: 0;
right: 0;
max-width: calc(100% - 16px);
max-height: calc(100% - 16px);
overflow: auto;
margin: auto;
::v-deep > * {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
max-height: 100%;
max-width: 100%;
}
}
}
</style>

View File

@@ -1,33 +1,36 @@
<template>
<header class="kkwtjztg">
<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">
<mk-user-name :user="note.user"/>
<router-link class="name" :to="userPage(note.user)" v-user-preview="note.user.id">
<MkUserName :user="note.user"/>
</router-link>
<span class="is-bot" v-if="note.user.isBot">bot</span>
<span class="username"><mk-acct :user="note.user"/></span>
<span class="admin" v-if="note.user.isAdmin"><fa :icon="faBookmark"/></span>
<span class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><fa :icon="farBookmark"/></span>
<span class="username"><MkAcct :user="note.user"/></span>
<span class="admin" v-if="note.user.isAdmin"><Fa :icon="faBookmark"/></span>
<span class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><Fa :icon="farBookmark"/></span>
<div class="info">
<span class="mobile" v-if="note.viaMobile"><fa :icon="faMobileAlt"/></span>
<router-link class="created-at" :to="note | notePage">
<mk-time :time="note.createdAt"/>
<span class="mobile" v-if="note.viaMobile"><Fa :icon="faMobileAlt"/></span>
<router-link class="created-at" :to="notePage(note)">
<MkTime :time="note.createdAt"/>
</router-link>
<span class="visibility" v-if="note.visibility !== 'public'">
<fa v-if="note.visibility === 'home'" :icon="faHome"/>
<fa v-if="note.visibility === 'followers'" :icon="faUnlock"/>
<fa v-if="note.visibility === 'specified'" :icon="faEnvelope"/>
<Fa v-if="note.visibility === 'home'" :icon="faHome"/>
<Fa v-if="note.visibility === 'followers'" :icon="faUnlock"/>
<Fa v-if="note.visibility === 'specified'" :icon="faEnvelope"/>
</span>
<span class="localOnly" v-if="note.localOnly"><fa :icon="faBiohazard"/></span>
<span class="localOnly" v-if="note.localOnly"><Fa :icon="faBiohazard"/></span>
</div>
</header>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faHome, faUnlock, faEnvelope, faMobileAlt, faBookmark, faBiohazard } from '@fortawesome/free-solid-svg-icons';
import { faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons';
import notePage from '../filters/note';
import { userPage } from '../filters/user';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
note: {
type: Object,
@@ -39,6 +42,11 @@ export default Vue.extend({
return {
faHome, faUnlock, faEnvelope, faMobileAlt, faBookmark, farBookmark, faBiohazard
};
},
methods: {
notePage,
userPage
}
});
</script>

View File

@@ -1,15 +1,15 @@
<template>
<div class="yohlumlk">
<mk-avatar class="avatar" :user="note.user"/>
<MkAvatar class="avatar" :user="note.user"/>
<div class="main">
<x-note-header class="header" :note="note" :mini="true"/>
<XNoteHeader class="header" :note="note" :mini="true"/>
<div class="body">
<p v-if="note.cw != null" class="cw">
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
<x-cw-button v-model="showContent" :note="note"/>
<XCwButton v-model:value="showContent" :note="note"/>
</p>
<div class="content" v-show="note.cw == null || showContent">
<x-sub-note-content class="text" :note="note"/>
<XSubNote-content class="text" :note="note"/>
</div>
</div>
</div>
@@ -17,12 +17,13 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import XNoteHeader from './note-header.vue';
import XSubNoteContent from './sub-note-content.vue';
import XCwButton from './cw-button.vue';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
XNoteHeader,
XSubNoteContent,

View File

@@ -1,32 +1,33 @@
<template>
<div class="wrpstxzv" :class="{ children }" v-size="[{ max: 450 }]">
<div class="wrpstxzv" :class="{ children }" v-size="{ max: [450] }">
<div class="main">
<mk-avatar class="avatar" :user="note.user"/>
<MkAvatar class="avatar" :user="note.user"/>
<div class="body">
<x-note-header class="header" :note="note" :mini="true"/>
<XNoteHeader class="header" :note="note" :mini="true"/>
<div class="body">
<p v-if="note.cw != null" class="cw">
<mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
<x-cw-button v-model="showContent" :note="note"/>
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
<XCwButton v-model:value="showContent" :note="note"/>
</p>
<div class="content" v-show="note.cw == null || showContent">
<x-sub-note-content class="text" :note="note"/>
<XSubNote-content class="text" :note="note"/>
</div>
</div>
</div>
</div>
<x-sub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :children="true"/>
<XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :children="true"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import XNoteHeader from './note-header.vue';
import XSubNoteContent from './sub-note-content.vue';
import XCwButton from './cw-button.vue';
import * as os from '@/os';
export default Vue.extend({
name: 'x-sub',
export default defineComponent({
name: 'XSub',
components: {
XNoteHeader,
@@ -65,7 +66,7 @@ export default Vue.extend({
created() {
if (this.detail) {
this.$root.api('notes/children', {
os.api('notes/children', {
noteId: this.note.id,
limit: 5
}).then(replies => {

View File

@@ -6,97 +6,102 @@
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }"
v-hotkey="keymap"
v-size="[{ max: 500 }, { max: 450 }, { max: 350 }, { max: 300 }]"
v-size="{ max: [500, 450, 350, 300] }"
>
<x-sub v-for="note in conversation" class="reply-to-more" :key="note.id" :note="note"/>
<x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
<div class="info" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div>
<div class="info" v-if="appearNote._prId_"><fa :icon="faBullhorn"/> {{ $t('promotion') }}<button class="_textButton hide" @click="readPromo()">{{ $t('hideThisNote') }} <fa :icon="faTimes"/></button></div>
<div class="info" v-if="appearNote._featuredId_"><fa :icon="faBolt"/> {{ $t('featured') }}</div>
<XSub v-for="note in conversation" class="reply-to-more" :key="note.id" :note="note"/>
<XSub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
<div class="info" v-if="pinned"><Fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div>
<div class="info" v-if="appearNote._prId_"><Fa :icon="faBullhorn"/> {{ $t('promotion') }}<button class="_textButton hide" @click="readPromo()">{{ $t('hideThisNote') }} <Fa :icon="faTimes"/></button></div>
<div class="info" v-if="appearNote._featuredId_"><Fa :icon="faBolt"/> {{ $t('featured') }}</div>
<div class="renote" v-if="isRenote">
<mk-avatar class="avatar" :user="note.user"/>
<fa :icon="faRetweet"/>
<i18n path="renotedBy" tag="span">
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId" place="user">
<mk-user-name :user="note.user"/>
</router-link>
</i18n>
<MkAvatar class="avatar" :user="note.user"/>
<Fa :icon="faRetweet"/>
<i18n-t keypath="renotedBy" tag="span">
<template #user>
<router-link class="name" :to="userPage(note.user)" v-user-preview="note.userId">
<MkUserName :user="note.user"/>
</router-link>
</template>
</i18n-t>
<div class="info">
<button class="_button time" @click="showRenoteMenu()" ref="renoteTime">
<fa class="dropdownIcon" v-if="isMyRenote" :icon="faEllipsisH"/>
<mk-time :time="note.createdAt"/>
<Fa class="dropdownIcon" v-if="isMyRenote" :icon="faEllipsisH"/>
<MkTime :time="note.createdAt"/>
</button>
<span class="visibility" v-if="note.visibility !== 'public'">
<fa v-if="note.visibility === 'home'" :icon="faHome"/>
<fa v-if="note.visibility === 'followers'" :icon="faUnlock"/>
<fa v-if="note.visibility === 'specified'" :icon="faEnvelope"/>
<Fa v-if="note.visibility === 'home'" :icon="faHome"/>
<Fa v-if="note.visibility === 'followers'" :icon="faUnlock"/>
<Fa v-if="note.visibility === 'specified'" :icon="faEnvelope"/>
</span>
<span class="localOnly" v-if="note.localOnly"><fa :icon="faBiohazard"/></span>
<span class="localOnly" v-if="note.localOnly"><Fa :icon="faBiohazard"/></span>
</div>
</div>
<article class="article">
<mk-avatar class="avatar" :user="appearNote.user"/>
<article class="article" @contextmenu="onContextmenu">
<MkAvatar class="avatar" :user="appearNote.user"/>
<div class="main">
<x-note-header class="header" :note="appearNote" :mini="true"/>
<XNoteHeader class="header" :note="appearNote" :mini="true"/>
<div class="body" ref="noteBody">
<p v-if="appearNote.cw != null" class="cw">
<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
<x-cw-button v-model="showContent" :note="appearNote"/>
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
<XCwButton v-model:value="showContent" :note="appearNote"/>
</p>
<div class="content" v-show="appearNote.cw == null || showContent">
<div class="text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link>
<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><Fa :icon="faReply"/></router-link>
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
<a class="rp" v-if="appearNote.renote != null">RN:</a>
</div>
<div class="files" v-if="appearNote.files.length > 0">
<x-media-list :media-list="appearNote.files" :parent-element="noteBody"/>
<XMediaList :media-list="appearNote.files" :parent-element="noteBody"/>
</div>
<x-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="detail" class="url-preview"/>
<div class="renote" v-if="appearNote.renote"><x-note-preview :note="appearNote.renote"/></div>
<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="detail" class="url-preview"/>
<div class="renote" v-if="appearNote.renote"><XNotePreview :note="appearNote.renote"/></div>
</div>
<router-link v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><Fa :icon="faSatelliteDish"/> {{ appearNote.channel.name }}</router-link>
</div>
<footer class="footer">
<x-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
<XReactionsViewer :note="appearNote" ref="reactionsViewer"/>
<button @click="reply()" class="button _button">
<template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template>
<template v-else><fa :icon="faReply"/></template>
<template v-if="appearNote.reply"><Fa :icon="faReplyAll"/></template>
<template v-else><Fa :icon="faReply"/></template>
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
</button>
<button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton">
<fa :icon="faRetweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
<Fa :icon="faRetweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
</button>
<button v-else class="button _button">
<fa :icon="faBan"/>
<Fa :icon="faBan"/>
</button>
<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
<fa :icon="faPlus"/>
<Fa :icon="faPlus"/>
</button>
<button v-if="appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton">
<fa :icon="faMinus"/>
<Fa :icon="faMinus"/>
</button>
<button class="button _button" @click="menu()" ref="menuButton">
<fa :icon="faEllipsisH"/>
<Fa :icon="faEllipsisH"/>
</button>
</footer>
</div>
</article>
<x-sub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
<XSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
</div>
<div v-else class="_panel muted" @click="muted = false">
<i18n path="userSaysSomething" tag="small">
<router-link class="name" :to="appearNote.user | userPage" v-user-preview="appearNote.userId" place="name">
<mk-user-name :user="appearNote.user"/>
</router-link>
</i18n>
<i18n-t keypath="userSaysSomething" tag="small">
<template #name>
<router-link class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId">
<MkUserName :user="appearNote.user"/>
</router-link>
</template>
</i18n-t>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons';
import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue';
import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons';
import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
import { parse } from '../../mfm/parse';
import { sum, unique } from '../../prelude/array';
@@ -107,21 +112,24 @@ import XReactionsViewer from './reactions-viewer.vue';
import XMediaList from './media-list.vue';
import XCwButton from './cw-button.vue';
import XPoll from './poll.vue';
import MkUrlPreview from './url-preview.vue';
import MkReactionPicker from './reaction-picker.vue';
import pleaseLogin from '../scripts/please-login';
import { focusPrev, focusNext } from '../scripts/focus';
import { url } from '../config';
import copyToClipboard from '../scripts/copy-to-clipboard';
import { checkWordMute } from '../scripts/check-word-mute';
import { utils } from '@syuilo/aiscript';
import { pleaseLogin } from '@/scripts/please-login';
import { focusPrev, focusNext } from '@/scripts/focus';
import { url } from '@/config';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import { checkWordMute } from '@/scripts/check-word-mute';
import { userPage } from '@/filters/user';
import * as os from '@/os';
import { noteActions, noteViewInterruptors } from '@/store';
export default Vue.extend({
model: {
prop: 'note',
event: 'updated'
},
function markRawAll(...xs) {
for (const x of xs) {
markRaw(x);
}
}
markRawAll(faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug, faSatelliteDish);
export default defineComponent({
components: {
XSub,
XNoteHeader,
@@ -130,7 +138,13 @@ export default Vue.extend({
XMediaList,
XCwButton,
XPoll,
MkUrlPreview,
MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
},
inject: {
inChannel: {
default: null
}
},
props: {
@@ -150,6 +164,8 @@ export default Vue.extend({
},
},
emits: ['update:note'],
data() {
return {
connection: null,
@@ -159,11 +175,14 @@ export default Vue.extend({
isDeleted: false,
muted: false,
noteBody: this.$refs.noteBody,
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug, faSatelliteDish
};
},
computed: {
rs() {
return this.$store.state.settings.reactions;
},
keymap(): any {
return {
'r': () => this.reply(true),
@@ -177,16 +196,16 @@ export default Vue.extend({
'esc': this.blur,
'm|o': () => this.menu(true),
's': this.toggleShowContent,
'1': () => this.reactDirectly(this.$store.state.settings.reactions[0]),
'2': () => this.reactDirectly(this.$store.state.settings.reactions[1]),
'3': () => this.reactDirectly(this.$store.state.settings.reactions[2]),
'4': () => this.reactDirectly(this.$store.state.settings.reactions[3]),
'5': () => this.reactDirectly(this.$store.state.settings.reactions[4]),
'6': () => this.reactDirectly(this.$store.state.settings.reactions[5]),
'7': () => this.reactDirectly(this.$store.state.settings.reactions[6]),
'8': () => this.reactDirectly(this.$store.state.settings.reactions[7]),
'9': () => this.reactDirectly(this.$store.state.settings.reactions[8]),
'0': () => this.reactDirectly(this.$store.state.settings.reactions[9]),
'1': () => this.reactDirectly(this.rs[0]),
'2': () => this.reactDirectly(this.rs[1]),
'3': () => this.reactDirectly(this.rs[2]),
'4': () => this.reactDirectly(this.rs[3]),
'5': () => this.reactDirectly(this.rs[4]),
'6': () => this.reactDirectly(this.rs[5]),
'7': () => this.reactDirectly(this.rs[6]),
'8': () => this.reactDirectly(this.rs[7]),
'9': () => this.reactDirectly(this.rs[8]),
'0': () => this.reactDirectly(this.rs[9]),
};
},
@@ -244,22 +263,22 @@ export default Vue.extend({
async created() {
if (this.$store.getters.isSignedIn) {
this.connection = this.$root.stream;
this.connection = os.stream;
}
// plugin
if (this.$store.state.noteViewInterruptors.length > 0) {
if (noteViewInterruptors.length > 0) {
let result = this.note;
for (const interruptor of this.$store.state.noteViewInterruptors) {
result = utils.valToJs(await interruptor.handler(JSON.parse(JSON.stringify(result))));
for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
}
this.$emit('updated', Object.freeze(result));
this.$emit('update:note', Object.freeze(result));
}
this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords);
if (this.detail) {
this.$root.api('notes/children', {
os.api('notes/children', {
noteId: this.appearNote.id,
limit: 30
}).then(replies => {
@@ -267,7 +286,7 @@ export default Vue.extend({
});
if (this.appearNote.replyId) {
this.$root.api('notes/conversation', {
os.api('notes/conversation', {
noteId: this.appearNote.replyId
}).then(conversation => {
this.conversation = conversation.reverse();
@@ -286,7 +305,7 @@ export default Vue.extend({
this.noteBody = this.$refs.noteBody;
},
beforeDestroy() {
beforeUnmount() {
this.decapture(true);
if (this.$store.getters.isSignedIn) {
@@ -296,7 +315,7 @@ export default Vue.extend({
methods: {
updateAppearNote(v) {
this.$emit('updated', Object.freeze(this.isRenote ? {
this.$emit('update:note', Object.freeze(this.isRenote ? {
...this.note,
renote: {
...this.note.renote,
@@ -309,7 +328,7 @@ export default Vue.extend({
},
readPromo() {
(this as any).$root.api('promo/read', {
os.api('promo/read', {
noteId: this.appearNote.id
});
this.isDeleted = true;
@@ -432,8 +451,8 @@ export default Vue.extend({
},
reply(viaKeyboard = false) {
pleaseLogin(this.$root);
this.$root.post({
pleaseLogin();
os.post({
reply: this.appearNote,
animation: !viaKeyboard,
}, () => {
@@ -442,57 +461,56 @@ export default Vue.extend({
},
renote(viaKeyboard = false) {
pleaseLogin(this.$root);
pleaseLogin();
this.blur();
this.$root.menu({
items: [{
text: this.$t('renote'),
icon: faRetweet,
action: () => {
(this as any).$root.api('notes/create', {
renoteId: this.appearNote.id
});
}
}, {
text: this.$t('quote'),
icon: faQuoteRight,
action: () => {
this.$root.post({
renote: this.appearNote,
});
}
}]
source: this.$refs.renoteButton,
os.modalMenu([{
text: this.$t('renote'),
icon: faRetweet,
action: () => {
os.api('notes/create', {
renoteId: this.appearNote.id
});
}
}, {
text: this.$t('quote'),
icon: faQuoteRight,
action: () => {
os.post({
renote: this.appearNote,
});
}
}], this.$refs.renoteButton, {
viaKeyboard
});
},
renoteDirectly() {
(this as any).$root.api('notes/create', {
os.api('notes/create', {
renoteId: this.appearNote.id
});
},
react(viaKeyboard = false) {
pleaseLogin(this.$root);
pleaseLogin();
this.blur();
const picker = this.$root.new(MkReactionPicker, {
source: this.$refs.reactButton,
os.popup(defineAsyncComponent(() => import('@/components/reaction-picker.vue')), {
showFocus: viaKeyboard,
});
picker.$once('chosen', reaction => {
this.$root.api('notes/reactions/create', {
noteId: this.appearNote.id,
reaction: reaction
}).then(() => {
picker.close();
});
});
picker.$once('closed', this.focus);
src: this.$refs.reactButton,
}, {
done: reaction => {
if (reaction) {
os.api('notes/reactions/create', {
noteId: this.appearNote.id,
reaction: reaction
});
}
this.focus();
},
}, 'closed');
},
reactDirectly(reaction) {
this.$root.api('notes/reactions/create', {
os.api('notes/reactions/create', {
noteId: this.appearNote.id,
reaction: reaction
});
@@ -501,81 +519,67 @@ export default Vue.extend({
undoReact(note) {
const oldReaction = note.myReaction;
if (!oldReaction) return;
this.$root.api('notes/reactions/delete', {
os.api('notes/reactions/delete', {
noteId: note.id
});
},
favorite() {
pleaseLogin(this.$root);
this.$root.api('notes/favorites/create', {
pleaseLogin();
os.apiWithDialog('notes/favorites/create', {
noteId: this.appearNote.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
});
},
del() {
this.$root.dialog({
os.dialog({
type: 'warning',
text: this.$t('noteDeleteConfirm'),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('notes/delete', {
os.api('notes/delete', {
noteId: this.appearNote.id
});
});
},
delEdit() {
this.$root.dialog({
os.dialog({
type: 'warning',
text: this.$t('deleteAndEditConfirm'),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('notes/delete', {
os.api('notes/delete', {
noteId: this.appearNote.id
});
this.$root.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply });
os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
});
},
toggleFavorite(favorite: boolean) {
this.$root.api(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
noteId: this.appearNote.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
});
},
toggleWatch(watch: boolean) {
this.$root.api(watch ? 'notes/watching/create' : 'notes/watching/delete', {
os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
noteId: this.appearNote.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
});
},
async menu(viaKeyboard = false) {
getMenu() {
let menu;
if (this.$store.getters.isSignedIn) {
const state = await this.$root.api('notes/state', {
const statePromise = os.api('notes/state', {
noteId: this.appearNote.id
});
menu = [{
type: 'link',
icon: faInfoCircle,
@@ -597,7 +601,7 @@ export default Vue.extend({
}
} : undefined,
null,
state.isFavorited ? {
statePromise.then(state => state.isFavorited ? {
icon: faStar,
text: this.$t('unfavorite'),
action: () => this.toggleFavorite(false)
@@ -605,8 +609,8 @@ export default Vue.extend({
icon: faStar,
text: this.$t('favorite'),
action: () => this.toggleFavorite(true)
},
this.appearNote.userId != this.$store.state.i.id ? state.isWatching ? {
}),
(this.appearNote.userId != this.$store.state.i.id) ? statePromise.then(state => state.isWatching ? {
icon: faEyeSlash,
text: this.$t('unwatch'),
action: () => this.toggleWatch(false)
@@ -614,7 +618,7 @@ export default Vue.extend({
icon: faEye,
text: this.$t('watch'),
action: () => this.toggleWatch(true)
} : undefined,
}) : undefined,
this.appearNote.userId == this.$store.state.i.id ? (this.$store.state.i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
icon: faThumbtack,
text: this.$t('unpin'),
@@ -643,6 +647,7 @@ export default Vue.extend({
{
icon: faTrashAlt,
text: this.$t('delete'),
danger: true,
action: this.del
}]
: []
@@ -667,8 +672,8 @@ export default Vue.extend({
.filter(x => x !== undefined);
}
if (this.$store.state.noteActions.length > 0) {
menu = menu.concat([null, ...this.$store.state.noteActions.map(action => ({
if (noteActions.length > 0) {
menu = menu.concat([null, ...noteActions.map(action => ({
icon: faPlug,
text: action.title,
action: () => {
@@ -677,27 +682,39 @@ export default Vue.extend({
}))]);
}
this.$root.menu({
items: menu,
source: this.$refs.menuButton,
return menu;
},
onContextmenu(e) {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(e.target)) return;
if (window.getSelection().toString() !== '') return;
os.contextMenu(this.getMenu(), e).then(this.focus);
},
menu(viaKeyboard = false) {
os.modalMenu(this.getMenu(), this.$refs.menuButton, {
viaKeyboard
}).then(this.focus);
},
showRenoteMenu(viaKeyboard = false) {
if (!this.isMyRenote) return;
this.$root.menu({
items: [{
text: this.$t('unrenote'),
icon: faTrashAlt,
action: () => {
this.$root.api('notes/delete', {
noteId: this.note.id
});
this.isDeleted = true;
}
}],
source: this.$refs.renoteTime,
os.modalMenu([{
text: this.$t('unrenote'),
icon: faTrashAlt,
action: () => {
os.api('notes/delete', {
noteId: this.note.id
});
this.isDeleted = true;
}
}], this.$refs.renoteTime, {
viaKeyboard: viaKeyboard
});
},
@@ -708,31 +725,20 @@ export default Vue.extend({
copyContent() {
copyToClipboard(this.appearNote.text);
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
os.success();
},
copyLink() {
copyToClipboard(`${url}/notes/${this.appearNote.id}`);
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
os.success();
},
togglePin(pin: boolean) {
this.$root.api(pin ? 'i/pin' : 'i/unpin', {
os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
noteId: this.appearNote.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}).catch(e => {
}, undefined, null, e => {
if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
this.$root.dialog({
os.dialog({
type: 'error',
text: this.$t('pinLimitExceeded')
});
@@ -741,26 +747,16 @@ export default Vue.extend({
},
async promote() {
const { canceled, result: days } = await this.$root.dialog({
const { canceled, result: days } = await os.dialog({
title: this.$t('numberOfDays'),
input: { type: 'number' }
});
if (canceled) return;
this.$root.api('admin/promo/create', {
os.apiWithDialog('admin/promo/create', {
noteId: this.appearNote.id,
expiresAt: Date.now() + (86400000 * days)
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
},
@@ -778,7 +774,9 @@ export default Vue.extend({
focusAfter() {
focusNext(this.$el);
}
},
userPage
}
});
</script>
@@ -788,10 +786,28 @@ export default Vue.extend({
position: relative;
transition: box-shadow 0.1s ease;
overflow: hidden;
contain: content;
&:focus {
outline: none;
box-shadow: 0 0 0 3px var(--focus);
&:after {
content: "";
pointer-events: none;
display: block;
position: absolute;
z-index: 10;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
width: calc(100% - 8px);
height: calc(100% - 8px);
border: dashed 1px var(--focus);
border-radius: var(--radius);
box-sizing: border-box;
}
}
&:hover > .article > .main > .footer > .button {
@@ -954,6 +970,11 @@ export default Vue.extend({
}
}
}
> .channel {
opacity: 0.7;
font-size: 80%;
}
}
> .footer {

View File

@@ -1,42 +1,41 @@
<template>
<div class="mk-notes">
<div class="_list_">
<div class="_fullinfo" v-if="empty">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ $t('noNotes') }}</div>
</div>
<mk-error v-if="error" @retry="init()"/>
<MkError v-if="error" @retry="init()"/>
<div v-show="more && reversed" style="margin-bottom: var(--margin);">
<button class="_panel _button" ref="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><mk-loading inline/></template>
<template v-if="moreFetching"><MkLoading inline/></template>
</button>
</div>
<x-list ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
<x-note :note="note" @updated="updated(note, $event)" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
</x-list>
<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
<XNote :note="note" @update:note="updated(note, $event)" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
</XList>
<div v-show="more && !reversed" style="margin-top: var(--margin);">
<button class="_panel _button" ref="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><mk-loading inline/></template>
<template v-if="moreFetching"><MkLoading inline/></template>
</button>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import paging from '../scripts/paging';
import { defineComponent } from 'vue';
import paging from '@/scripts/paging';
import XNote from './note.vue';
import XList from './date-separated-list.vue';
import MkButton from './ui/button.vue';
export default Vue.extend({
export default defineComponent({
components: {
XNote, XList, MkButton
XNote, XList,
},
mixins: [
@@ -68,6 +67,8 @@ export default Vue.extend({
}
},
emits: ['before', 'after'],
computed: {
notes(): any[] {
return this.prop ? this.items.map(item => item[this.prop]) : this.items;
@@ -82,9 +83,9 @@ export default Vue.extend({
updated(oldValue, newValue) {
const i = this.notes.findIndex(n => n === oldValue);
if (this.prop) {
Vue.set(this.items[i], this.prop, newValue);
this.items[i][this.prop] = newValue;
} else {
Vue.set(this.items, i, newValue);
this.items[i] = newValue;
}
},
@@ -94,4 +95,3 @@ export default Vue.extend({
}
});
</script>

View File

@@ -0,0 +1,97 @@
<template>
<XModalWindow ref="dialog"
:width="400"
:height="450"
:with-ok-button="true"
:ok-button-disabled="false"
@ok="ok()"
@close="$refs.dialog.close()"
@closed="$emit('closed')"
>
<template #header>{{ $t('notificationSetting') }}</template>
<div v-if="showGlobalToggle" class="_section">
<MkSwitch v-model:value="useGlobalSetting">
{{ $t('useGlobalSetting') }}
<template #desc>{{ $t('useGlobalSettingDesc') }}</template>
</MkSwitch>
</div>
<div v-if="!useGlobalSetting" class="_section">
<MkInfo>{{ $t('notificationSettingDesc') }}</MkInfo>
<MkButton inline @click="disableAll">{{ $t('disableAll') }}</MkButton>
<MkButton inline @click="enableAll">{{ $t('enableAll') }}</MkButton>
<MkSwitch v-for="type in notificationTypes" :key="type" v-model:value="typesMap[type]">{{ $t(`_notification._types.${type}`) }}</MkSwitch>
</div>
</XModalWindow>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkSwitch from './ui/switch.vue';
import MkInfo from './ui/info.vue';
import MkButton from './ui/button.vue';
import { notificationTypes } from '../../types';
export default defineComponent({
components: {
XModalWindow,
MkSwitch,
MkInfo,
MkButton
},
props: {
includingTypes: {
// TODO: これで型に合わないものを弾いてくれるのかどうか要調査
type: Array as PropType<typeof notificationTypes[number][]>,
required: false,
default: null,
},
showGlobalToggle: {
type: Boolean,
required: false,
default: true,
}
},
emits: ['done', 'closed'],
data() {
return {
typesMap: {} as Record<typeof notificationTypes[number], boolean>,
useGlobalSetting: false,
notificationTypes,
};
},
created() {
this.useGlobalSetting = this.includingTypes === null && this.showGlobalToggle;
for (const type of this.notificationTypes) {
this.typesMap[type] = this.includingTypes === null || this.includingTypes.includes(type);
}
},
methods: {
ok() {
const includingTypes = this.useGlobalSetting ? null : (Object.keys(this.typesMap) as typeof notificationTypes[number][])
.filter(type => this.typesMap[type]);
this.$emit('done', { includingTypes });
this.$refs.dialog.close();
},
disableAll() {
for (const type in this.typesMap) {
this.typesMap[type as typeof notificationTypes[number]] = false;
}
},
enableAll() {
for (const type in this.typesMap) {
this.typesMap[type as typeof notificationTypes[number]] = true;
}
}
}
});
</script>

View File

@@ -1,71 +1,75 @@
<template>
<div class="qglefbjs" :class="notification.type" v-size="[{ max: 500 }, { max: 600 }]">
<div class="qglefbjs" :class="notification.type" v-size="{ max: [500, 600] }">
<div class="head">
<mk-avatar v-if="notification.user" class="icon" :user="notification.user"/>
<img v-else class="icon" :src="notification.icon" alt=""/>
<MkAvatar v-if="notification.user" class="icon" :user="notification.user"/>
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
<div class="sub-icon" :class="notification.type">
<fa :icon="faPlus" v-if="notification.type === 'follow'"/>
<fa :icon="faClock" v-else-if="notification.type === 'receiveFollowRequest'"/>
<fa :icon="faCheck" v-else-if="notification.type === 'followRequestAccepted'"/>
<fa :icon="faIdCardAlt" v-else-if="notification.type === 'groupInvited'"/>
<fa :icon="faRetweet" v-else-if="notification.type === 'renote'"/>
<fa :icon="faReply" v-else-if="notification.type === 'reply'"/>
<fa :icon="faAt" v-else-if="notification.type === 'mention'"/>
<fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/>
<fa :icon="faPollH" v-else-if="notification.type === 'pollVote'"/>
<x-reaction-icon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :custom-emojis="notification.note.emojis" :no-style="true"/>
<Fa :icon="faPlus" v-if="notification.type === 'follow'"/>
<Fa :icon="faClock" v-else-if="notification.type === 'receiveFollowRequest'"/>
<Fa :icon="faCheck" v-else-if="notification.type === 'followRequestAccepted'"/>
<Fa :icon="faIdCardAlt" v-else-if="notification.type === 'groupInvited'"/>
<Fa :icon="faRetweet" v-else-if="notification.type === 'renote'"/>
<Fa :icon="faReply" v-else-if="notification.type === 'reply'"/>
<Fa :icon="faAt" v-else-if="notification.type === 'mention'"/>
<Fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/>
<Fa :icon="faPollH" v-else-if="notification.type === 'pollVote'"/>
<XReactionIcon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :custom-emojis="notification.note.emojis" :no-style="true"/>
</div>
</div>
<div class="tail">
<header>
<router-link v-if="notification.user" class="name" :to="notification.user | userPage" v-user-preview="notification.user.id"><mk-user-name :user="notification.user"/></router-link>
<router-link v-if="notification.user" class="name" :to="userPage(notification.user)" v-user-preview="notification.user.id"><MkUserName :user="notification.user"/></router-link>
<span v-else>{{ notification.header }}</span>
<mk-time :time="notification.createdAt" v-if="withTime"/>
<MkTime :time="notification.createdAt" v-if="withTime"/>
</header>
<router-link v-if="notification.type === 'reaction'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<fa :icon="faQuoteLeft"/>
<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
<fa :icon="faQuoteRight"/>
<router-link v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Fa :icon="faQuoteLeft"/>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
<Fa :icon="faQuoteRight"/>
</router-link>
<router-link v-if="notification.type === 'renote'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
<fa :icon="faQuoteLeft"/>
<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.renote.emojis"/>
<fa :icon="faQuoteRight"/>
<router-link v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
<Fa :icon="faQuoteLeft"/>
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.renote.emojis"/>
<Fa :icon="faQuoteRight"/>
</router-link>
<router-link v-if="notification.type === 'reply'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
<router-link v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
</router-link>
<router-link v-if="notification.type === 'mention'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
<router-link v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
</router-link>
<router-link v-if="notification.type === 'quote'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
<router-link v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
</router-link>
<router-link v-if="notification.type === 'pollVote'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<fa :icon="faQuoteLeft"/>
<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
<fa :icon="faQuoteRight"/>
<router-link v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Fa :icon="faQuoteLeft"/>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
<Fa :icon="faQuoteRight"/>
</router-link>
<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $t('youGotNewFollower') }}<div v-if="full"><mk-follow-button :user="notification.user" :full="true"/></div></span>
<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $t('youGotNewFollower') }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $t('followRequestAccepted') }}</span>
<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $t('receiveFollowRequest') }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $t('reject') }}</button></div></span>
<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ $t('groupInvited') }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ $t('reject') }}</button></div></span>
<span v-if="notification.type === 'app'" class="text">
<mfm :text="notification.body" :nowrap="!full"/>
<Mfm :text="notification.body" :nowrap="!full"/>
</span>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faIdCardAlt, faPlus, faQuoteLeft, faQuoteRight, faRetweet, faReply, faAt, faCheck, faPollH } from '@fortawesome/free-solid-svg-icons';
import { faClock } from '@fortawesome/free-regular-svg-icons';
import noteSummary from '../../misc/get-note-summary';
import XReactionIcon from './reaction-icon.vue';
import MkFollowButton from './follow-button.vue';
import notePage from '../filters/note';
import { userPage } from '../filters/user';
import { locale } from '../i18n';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
XReactionIcon, MkFollowButton
},
@@ -87,7 +91,7 @@ export default Vue.extend({
},
data() {
return {
getNoteSummary: (text: string) => noteSummary(text, this.$root.i18n.messages[this.$root.i18n.locale]),
getNoteSummary: (text: string) => noteSummary(text, locale),
followRequestDone: false,
groupInviteDone: false,
connection: null,
@@ -100,7 +104,7 @@ export default Vue.extend({
if (!this.notification.isRead) {
this.readObserver = new IntersectionObserver((entries, observer) => {
if (!entries.some(entry => entry.isIntersecting)) return;
this.$root.stream.send('readNotification', {
os.stream.send('readNotification', {
id: this.notification.id
});
entries.map(({ target }) => observer.unobserve(target));
@@ -108,12 +112,12 @@ export default Vue.extend({
this.readObserver.observe(this.$el);
this.connection = this.$root.stream.useSharedConnection('main');
this.connection = os.stream.useSharedConnection('main');
this.connection.on('readAllNotifications', () => this.readObserver.unobserve(this.$el));
}
},
beforeDestroy() {
beforeUnmount() {
if (!this.notification.isRead) {
this.readObserver.unobserve(this.$el);
this.connection.dispose();
@@ -123,24 +127,22 @@ export default Vue.extend({
methods: {
acceptFollowRequest() {
this.followRequestDone = true;
this.$root.api('following/requests/accept', { userId: this.notification.user.id });
os.api('following/requests/accept', { userId: this.notification.user.id });
},
rejectFollowRequest() {
this.followRequestDone = true;
this.$root.api('following/requests/reject', { userId: this.notification.user.id });
os.api('following/requests/reject', { userId: this.notification.user.id });
},
acceptGroupInvitation() {
this.groupInviteDone = true;
this.$root.api('users/groups/invitations/accept', { invitationId: this.notification.invitation.id });
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
os.apiWithDialog('users/groups/invitations/accept', { invitationId: this.notification.invitation.id });
},
rejectGroupInvitation() {
this.groupInviteDone = true;
this.$root.api('users/groups/invitations/reject', { invitationId: this.notification.invitation.id });
os.api('users/groups/invitations/reject', { invitationId: this.notification.invitation.id });
},
notePage,
userPage
}
});
</script>
@@ -153,6 +155,7 @@ export default Vue.extend({
font-size: 0.9em;
overflow-wrap: break-word;
display: flex;
contain: content;
&.max-width_600px {
padding: 16px;

View File

@@ -1,29 +1,31 @@
<template>
<div class="mfcuwfyp">
<x-list class="notifications" :items="items" v-slot="{ item: notification }">
<x-note v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @updated="noteUpdated(notification.note, $event)" :key="notification.id"/>
<x-notification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
</x-list>
<XList class="notifications" :items="items" v-slot="{ item: notification }">
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/>
<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
</XList>
<button class="_panel _button" ref="loadMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><mk-loading inline/></template>
<template v-if="moreFetching"><MkLoading inline/></template>
</button>
<p class="empty" v-if="empty">{{ $t('noNotifications') }}</p>
<mk-error v-if="error" @retry="init()"/>
<MkError v-if="error" @retry="init()"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import paging from '../scripts/paging';
import { defineComponent, PropType } from 'vue';
import paging from '@/scripts/paging';
import XNotification from './notification.vue';
import XList from './date-separated-list.vue';
import XNote from './note.vue';
import { notificationTypes } from '../../types';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
XNotification,
XList,
@@ -35,9 +37,10 @@ export default Vue.extend({
],
props: {
type: {
type: String,
required: false
includeTypes: {
type: Array as PropType<typeof notificationTypes[number][]>,
required: false,
default: null,
},
},
@@ -48,47 +51,69 @@ export default Vue.extend({
endpoint: 'i/notifications',
limit: 10,
params: () => ({
includeTypes: this.type ? [this.type] : undefined
includeTypes: this.allIncludeTypes || undefined,
})
},
};
},
computed: {
allIncludeTypes() {
return this.includeTypes ?? notificationTypes.filter(x => !this.$store.state.i.mutingNotificationTypes.includes(x));
}
},
watch: {
type() {
this.reload();
includeTypes: {
handler() {
this.reload();
},
deep: true
},
// TODO: vue/vuexのバグか仕様かは不明なものの、プロフィール更新するなどして $store.state.i が更新されると、
// mutingNotificationTypes に変化が無くてもこのハンドラーが呼び出され無駄なリロードが発生するのを直す
'$store.state.i.mutingNotificationTypes': {
handler() {
if (this.includeTypes === null) {
this.reload();
}
},
deep: true
}
},
mounted() {
this.connection = this.$root.stream.useSharedConnection('main');
this.connection = os.stream.useSharedConnection('main');
this.connection.on('notification', this.onNotification);
},
beforeDestroy() {
beforeUnmount() {
this.connection.dispose();
},
methods: {
onNotification(notification) {
if (document.visibilityState === 'visible') {
this.$root.stream.send('readNotification', {
const isMuted = !this.allIncludeTypes.includes(notification.type);
if (isMuted || document.visibilityState === 'visible') {
os.stream.send('readNotification', {
id: notification.id
});
}
this.prepend({
...notification,
isRead: document.visibilityState === 'visible'
});
if (!isMuted) {
this.prepend({
...notification,
isRead: document.visibilityState === 'visible'
});
}
},
noteUpdated(oldValue, newValue) {
const i = this.items.findIndex(n => n.note === oldValue);
Vue.set(this.items, i, {
this.items[i] = {
...this.items[i],
note: newValue
});
};
},
}
});

View File

@@ -8,22 +8,27 @@
<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
<footer>
<img class="icon" :src="page.user.avatarUrl"/>
<p>{{ page.user | userName }}</p>
<p>{{ userName(page.user) }}</p>
</footer>
</article>
</router-link>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { userName } from '../filters/user';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
page: {
type: Object,
required: true
},
},
methods: {
userName
}
});
</script>

View File

@@ -0,0 +1,86 @@
<template>
<XWindow ref="window" :initial-width="400" :initial-height="450" :can-resize="true" @closed="$emit('closed')">
<template #header>
<XHeader :info="pageInfo" :with-back="false"/>
</template>
<template #buttons>
<button class="_button" @click="expand" v-tooltip="$t('showInPage')"><Fa :icon="faExpandAlt"/></button>
<button class="_button" @click="popout" v-tooltip="$t('popout')"><Fa :icon="faExternalLinkAlt"/></button>
</template>
<div style="min-height: 100%; background: var(--bg);">
<component :is="component" v-bind="props" :ref="changePage"/>
</div>
</XWindow>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import { faExternalLinkAlt, faExpandAlt } from '@fortawesome/free-solid-svg-icons';
import XWindow from '@/components/ui/window.vue';
import XHeader from '@/ui/_common_/header.vue';
import { popout } from '@/scripts/popout';
export default defineComponent({
components: {
XWindow,
XHeader,
},
props: {
initialUrl: {
type: String,
required: true,
},
initialComponent: {
type: Object,
required: true,
},
initialProps: {
type: Object,
required: false,
default: {},
},
},
emits: ['closed'],
data() {
return {
pageInfo: null,
url: this.initialUrl,
component: this.initialComponent,
props: this.initialProps,
faExternalLinkAlt, faExpandAlt,
};
},
provide() {
return {
navHook: (url, component, props) => {
this.url = url;
this.component = markRaw(component);
this.props = props;
}
};
},
methods: {
changePage(page) {
if (page == null) return;
if (page.INFO) {
this.pageInfo = page.INFO;
}
},
expand() {
this.$router.push(this.url);
this.$refs.window.close();
},
popout() {
popout(this.url, this.$el);
this.$refs.window.close();
},
},
});
</script>

View File

@@ -3,7 +3,7 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import XText from './page.text.vue';
import XSection from './page.section.vue';
import XImage from './page.image.vue';
@@ -19,7 +19,7 @@ import XCounter from './page.counter.vue';
import XRadioButton from './page.radio-button.vue';
import XCanvas from './page.canvas.vue';
export default Vue.extend({
export default defineComponent({
components: {
XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas
},

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