Compare commits

..

159 Commits

Author SHA1 Message Date
382b1d2250 8.57.1 2018-09-21 08:40:12 +09:00
629693355a Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-09-21 08:37:39 +09:00
00a3f8d392 Fix #2741 2018-09-21 08:37:26 +09:00
80b6e8090e fix(package): update @types/bcryptjs to version 2.4.2 (#2742) 2018-09-21 08:35:06 +09:00
a5f817d896 Fix #2744 2018-09-21 08:33:24 +09:00
51b0244cf2 fix(package): update websocket to version 1.0.28 (#2746)
Closes #2743
2018-09-21 04:30:29 +09:00
01131e2606 8.57.0 2018-09-20 17:23:55 +09:00
6283b7668e fix(package): update @types/koa-router to version 7.0.32 (#2740) 2018-09-20 17:23:26 +09:00
d058ecc4ea Resolve #2698 2018-09-20 17:21:16 +09:00
77a0450b5d 🎨 2018-09-20 16:21:51 +09:00
1dd1b9084f Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-09-20 16:16:08 +09:00
6341807d02 🎨 2018-09-20 16:16:01 +09:00
51a1f30225 fix(package): update @types/webpack to version 4.4.12 (#2739) 2018-09-20 06:29:03 +09:00
5422482696 Resolve #1153 2018-09-20 06:27:41 +09:00
cd7f8b080e Fix #2738 2018-09-20 04:56:24 +09:00
faf29b768f Make admin can delete any note 2018-09-19 17:29:03 +09:00
7576569dc9 8.56.0 2018-09-19 14:24:40 +09:00
ea3bcbbc37 キャッシュの設定を調整 2018-09-19 14:22:46 +09:00
d9f0e158a3 Implement #2736 2018-09-19 14:18:34 +09:00
195f676500 8.55.0 2018-09-19 08:38:18 +09:00
a9a2f4820b Add keyboard shortcut doc 2018-09-19 08:36:06 +09:00
8414db57f0 Specify AP Cache-Control (#2735) 2018-09-19 07:17:19 +09:00
609d68933e Add new shortcut 2018-09-19 02:51:06 +09:00
a23b8cebbc 8.54.0 2018-09-19 02:41:09 +09:00
89f6b03cd6 fix(package): update web-push to version 3.3.3 (#2733) 2018-09-19 02:39:57 +09:00
7bc9de03a6 fix(package): update webpack to version 4.19.1 (#2732) 2018-09-19 02:39:15 +09:00
3c865d6054 Add new shortcut 2018-09-19 02:35:32 +09:00
fd770b008e Add new shortcut 2018-09-19 02:32:44 +09:00
b0d60ef2c2 Add new shortcut 2018-09-19 02:27:19 +09:00
7b9cea06ef Fix 2018-09-19 02:26:06 +09:00
30608d3e22 8.53.0 2018-09-18 16:45:55 +09:00
8bf4e55338 Improve keyboard shortcuts 2018-09-18 16:45:20 +09:00
6ead1de383 Improve readability 2018-09-18 15:02:26 +09:00
3b628ec3c4 将来的にバグに繋がりかねない挙動を修正 2018-09-18 15:02:15 +09:00
0ed704d173 8.52.0 2018-09-18 14:54:01 +09:00
87b6ef0ec5 Improve keyboard shortcut 2018-09-18 14:53:17 +09:00
5184a07cf2 Improve usability 2018-09-18 14:50:13 +09:00
dba04cc59c Improve keyboard shortcuts 2018-09-18 14:43:54 +09:00
f4045fb5b3 Improve keyboard shortcuts 2018-09-18 14:39:18 +09:00
16c36163b4 Fix bug 2018-09-18 14:35:46 +09:00
1ac033ff18 Improve keyboard shortcut 2018-09-18 14:30:50 +09:00
ccfd48232a 8.51.0 2018-09-18 13:14:42 +09:00
429bf179dc Refactor: Better type annotations 2018-09-18 13:14:17 +09:00
8ba3fb13eb Fix bug 2018-09-18 13:12:41 +09:00
11496d887e Publish pinned notes (#2731) 2018-09-18 13:08:27 +09:00
bec48319ec 8.50.0 2018-09-18 12:43:24 +09:00
71a93b2b43 Refactor & Usability improvements 2018-09-18 12:42:56 +09:00
6ed3f9e414 リファクタリングなど 2018-09-18 12:34:41 +09:00
dc8f592c1f 8.49.0 2018-09-18 09:21:02 +09:00
f66c31c771 Improve usability & refactoring 2018-09-18 09:20:06 +09:00
55e2ae1408 Improve usability 2018-09-18 09:11:52 +09:00
19c72627fc Improve keyboard shortcut 2018-09-18 08:19:45 +09:00
2a4c53c3a4 8.48.0 2018-09-18 06:30:52 +09:00
1f2ebce8ed Resolve #1302 2018-09-18 06:29:47 +09:00
fcea9dacb7 Clean up: Remove unused import 2018-09-18 06:20:49 +09:00
908872f374 8.47.0 2018-09-18 05:36:14 +09:00
f688ceafb8 Merge pull request #2729 from syuilo/greenkeeper/@types/node-10.10.1
Update @types/node to the latest version 🚀
2018-09-18 05:35:49 +09:00
b47b5d6d8b Merge pull request #2728 from syuilo/l10n_develop
New Crowdin translations
2018-09-18 05:35:39 +09:00
31ce3aa312 キーボードショートカットを強化するなど 2018-09-18 05:35:06 +09:00
5b22d92e99 New translations ja-JP.yml (English) 2018-09-18 03:51:42 +09:00
df148e25da fix(package): update @types/node to version 10.10.1 2018-09-17 17:24:15 +00:00
4b26df5c3a New translations ja-JP.yml (English) 2018-09-18 02:19:32 +09:00
e765be4205 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-09-18 02:15:36 +09:00
f7d2457063 New translations ja-JP.yml (Norwegian) 2018-09-18 02:15:27 +09:00
6032d803aa New translations ja-JP.yml (Dutch) 2018-09-18 02:15:25 +09:00
0de371db38 New translations ja-JP.yml (Japanese, Kansai) 2018-09-18 02:15:22 +09:00
ce3797c4af 8.46.0 2018-09-18 02:15:19 +09:00
56dd8c298b New translations ja-JP.yml (Spanish) 2018-09-18 02:15:19 +09:00
3533257efe New translations ja-JP.yml (Russian) 2018-09-18 02:15:16 +09:00
dc2f08721d New translations ja-JP.yml (Portuguese) 2018-09-18 02:15:14 +09:00
66608a4131 New translations ja-JP.yml (Polish) 2018-09-18 02:15:11 +09:00
2fa90131eb New translations ja-JP.yml (Korean) 2018-09-18 02:15:08 +09:00
a51ed28db6 New translations ja-JP.yml (Italian) 2018-09-18 02:15:06 +09:00
5ec290663b New translations ja-JP.yml (German) 2018-09-18 02:15:03 +09:00
1374d6e34d New translations ja-JP.yml (French) 2018-09-18 02:15:00 +09:00
45ade17c58 New translations ja-JP.yml (English) 2018-09-18 02:14:57 +09:00
c753e26187 New translations ja-JP.yml (Chinese Simplified) 2018-09-18 02:14:54 +09:00
577929eed1 New translations ja-JP.yml (Catalan) 2018-09-18 02:14:51 +09:00
1fde8a8fb0 Merge pull request #2727 from syuilo/greenkeeper/@types/node-10.10.0
Update @types/node to the latest version 🚀
2018-09-18 02:14:32 +09:00
77e53cbf9e Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-09-18 02:14:16 +09:00
ab83e08bc7 メッセージタイムラインを追加 2018-09-18 02:14:12 +09:00
2fad6e6d5f Refactor 2018-09-18 02:13:42 +09:00
a3604a6c95 Merge pull request #2722 from syuilo/l10n_develop
New Crowdin translations
2018-09-17 23:12:54 +09:00
44f6fe6f1f Refactor: Extract shouldMuteThisNote function 2018-09-17 23:07:15 +09:00
311b4e90ca No lint when test 2018-09-17 22:51:25 +09:00
f5a937c523 Better hashtag parsing 2018-09-17 22:51:10 +09:00
0632a3ed3f fix(package): update @types/node to version 10.10.0 2018-09-17 08:10:08 +00:00
71bada97df 8.45.1 2018-09-17 12:19:54 +09:00
62509edcbe Refactor 2018-09-17 12:18:59 +09:00
f97cdfaa20 Fix #2725 2018-09-17 11:59:24 +09:00
67ec10e86d Add untilId param 2018-09-17 11:43:53 +09:00
481b3f2c58 New translations ja-JP.yml (English) 2018-09-17 09:11:27 +09:00
7d599a68ea pong 2018-09-17 09:07:46 +09:00
7ccff732b8 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-09-17 09:07:05 +09:00
7587c896d5 8.45.0 2018-09-17 09:06:56 +09:00
91297f1ab3 Merge pull request #2717 from syuilo/l10n_develop
New Crowdin translations
2018-09-17 09:06:25 +09:00
d872a16fe0 🎨 2018-09-17 09:05:51 +09:00
60aa35adf8 New translations ja-JP.yml (Norwegian) 2018-09-17 09:01:59 +09:00
5035b66773 New translations ja-JP.yml (Dutch) 2018-09-17 09:01:57 +09:00
fa9da8ecab New translations ja-JP.yml (Japanese, Kansai) 2018-09-17 09:01:54 +09:00
1f9bca7188 New translations ja-JP.yml (Spanish) 2018-09-17 09:01:52 +09:00
ffa5bdeb50 New translations ja-JP.yml (Russian) 2018-09-17 09:01:49 +09:00
e6bfb7398e New translations ja-JP.yml (Portuguese) 2018-09-17 09:01:47 +09:00
6def0c776f New translations ja-JP.yml (Polish) 2018-09-17 09:01:45 +09:00
24bae9eaed New translations ja-JP.yml (Korean) 2018-09-17 09:01:43 +09:00
fb5175a283 New translations ja-JP.yml (Italian) 2018-09-17 09:01:40 +09:00
6e49437154 New translations ja-JP.yml (German) 2018-09-17 09:01:38 +09:00
2511ed56ac New translations ja-JP.yml (French) 2018-09-17 09:01:35 +09:00
c4bfc99cf5 New translations ja-JP.yml (English) 2018-09-17 09:01:33 +09:00
4efe38440d New translations ja-JP.yml (Chinese Simplified) 2018-09-17 09:01:30 +09:00
4a5f2c3c40 New translations ja-JP.yml (Catalan) 2018-09-17 09:01:27 +09:00
109738ccb9 ハッシュタグタイムラインを実装 2018-09-17 09:00:20 +09:00
433dbe179d 8.44.1 2018-09-17 03:37:22 +09:00
b21b33831a Fix bug 2018-09-17 03:36:58 +09:00
020cc471da 8.44.0 2018-09-17 03:03:58 +09:00
43b47c4494 fontawesomeを5.3.1にアップデート (#2718) 2018-09-17 03:02:58 +09:00
8751d91794 Better stats page 2018-09-17 02:56:57 +09:00
374b276f5c Fix #2720 2018-09-17 02:45:30 +09:00
6138a74231 Fix #2101 2018-09-17 00:20:00 +09:00
25438c4d64 New translations ja-JP.yml (French) 2018-09-17 00:01:10 +09:00
ae6ce19886 8.43.0 2018-09-16 23:19:06 +09:00
e17a9bfd6f Merge pull request #2714 from syuilo/l10n_develop
New Crowdin translations
2018-09-16 23:17:22 +09:00
dc2055f5bc Fix bug 2018-09-16 23:15:15 +09:00
afeb8058b1 Add mentions column (Deck) 2018-09-16 23:15:02 +09:00
9299f99ac3 New translations ja-JP.yml (English) 2018-09-16 23:02:02 +09:00
858fc7ebcc New translations ja-JP.yml (Norwegian) 2018-09-16 22:51:36 +09:00
35089c65d3 New translations ja-JP.yml (Dutch) 2018-09-16 22:51:34 +09:00
643ca42829 New translations ja-JP.yml (Japanese, Kansai) 2018-09-16 22:51:31 +09:00
935dc4fe33 New translations ja-JP.yml (Spanish) 2018-09-16 22:51:29 +09:00
3a9e74feb1 New translations ja-JP.yml (Russian) 2018-09-16 22:51:27 +09:00
92e66fbf0c New translations ja-JP.yml (Portuguese) 2018-09-16 22:51:24 +09:00
a50515f569 New translations ja-JP.yml (Polish) 2018-09-16 22:51:22 +09:00
2f8f47acea New translations ja-JP.yml (Korean) 2018-09-16 22:51:19 +09:00
dcb296db93 New translations ja-JP.yml (Italian) 2018-09-16 22:51:17 +09:00
0bdae9ede7 New translations ja-JP.yml (German) 2018-09-16 22:51:15 +09:00
11290c2a0f New translations ja-JP.yml (French) 2018-09-16 22:51:13 +09:00
428b8f8669 New translations ja-JP.yml (English) 2018-09-16 22:51:10 +09:00
7ced10f84e New translations ja-JP.yml (Chinese Simplified) 2018-09-16 22:51:08 +09:00
8ac54139c9 New translations ja-JP.yml (Catalan) 2018-09-16 22:51:05 +09:00
32afe77a26 自分宛ての投稿をタイムラインで見れるように 2018-09-16 22:48:57 +09:00
6db8e33662 New translations ja-JP.yml (English) 2018-09-16 22:01:26 +09:00
569561f247 New translations ja-JP.yml (Norwegian) 2018-09-16 21:52:05 +09:00
d132d82acf New translations ja-JP.yml (Dutch) 2018-09-16 21:52:02 +09:00
9ba0db9372 New translations ja-JP.yml (Japanese, Kansai) 2018-09-16 21:51:59 +09:00
5d468b542d New translations ja-JP.yml (Spanish) 2018-09-16 21:51:57 +09:00
32273165c7 New translations ja-JP.yml (Russian) 2018-09-16 21:51:55 +09:00
46fdb75bf4 New translations ja-JP.yml (Portuguese) 2018-09-16 21:51:52 +09:00
baf381814b New translations ja-JP.yml (Polish) 2018-09-16 21:51:50 +09:00
e90387c14e New translations ja-JP.yml (Korean) 2018-09-16 21:51:48 +09:00
876790d499 New translations ja-JP.yml (Italian) 2018-09-16 21:51:45 +09:00
8b56edda4b New translations ja-JP.yml (German) 2018-09-16 21:51:43 +09:00
33352256d6 New translations ja-JP.yml (French) 2018-09-16 21:51:41 +09:00
e368ef11fa New translations ja-JP.yml (English) 2018-09-16 21:51:39 +09:00
045f7c3185 New translations ja-JP.yml (Chinese Simplified) 2018-09-16 21:51:36 +09:00
bf40e5a5c5 New translations ja-JP.yml (Catalan) 2018-09-16 21:51:34 +09:00
cda3635d97 enable-animations --> reduce-motion 2018-09-16 21:40:48 +09:00
2eb561f132 New translations ja-JP.yml (Portuguese) 2018-09-16 05:43:02 +09:00
b5f6465d61 New translations ja-JP.yml (Portuguese) 2018-09-16 05:31:03 +09:00
9725076c46 New translations ja-JP.yml (Portuguese) 2018-09-16 05:20:49 +09:00
133 changed files with 2613 additions and 572 deletions

View File

@ -78,7 +78,7 @@ gulp.task('build:copy', ['build:copy:views', 'build:copy:lang'], () =>
]).pipe(gulp.dest('./built/')) ]).pipe(gulp.dest('./built/'))
); );
gulp.task('test', ['lint', 'mocha']); gulp.task('test', ['mocha']);
gulp.task('lint', () => gulp.task('lint', () =>
gulp.src('./src/**/*.ts') gulp.src('./src/**/*.ts')

View File

@ -112,7 +112,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "ホーム" home: "ホーム"
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知" notifications: "通知"
list: "リスト" list: "リスト"
swap-left: "左に移動" swap-left: "左に移動"
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト" list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、" welcome-back: "おかえりなさい、"
adjective: "さん" adjective: "さん"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:

View File

@ -112,7 +112,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "Startseite" home: "Startseite"
local: "Lokal" local: "Lokal"
hybrid: "ソーシャル" hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "Global" global: "Global"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "Mitteilungen" notifications: "Mitteilungen"
list: "Listen" list: "Listen"
swap-left: "Nach links" swap-left: "Nach links"
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "Lokal" local: "Lokal"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "Global" global: "Global"
mentions: "あなた宛て"
messages: "メッセージ"
list: "Listen" list: "Listen"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、" welcome-back: "おかえりなさい、"
adjective: "さん" adjective: "さん"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:

View File

@ -110,9 +110,9 @@ common:
verified-user: "Verified account" verified-user: "Verified account"
disable-animated-mfm: "Disable animated texts in a post" disable-animated-mfm: "Disable animated texts in a post"
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "Always post with a warning about media attachment"
show-full-acct: "Do not omit the hostname from the username" show-full-acct: "Do not omit the hostname from the username"
enable-animations: "Enable animations" reduce-motion: "Reduce motion in UI"
this-setting-is-this-device-only: "Only for this device" this-setting-is-this-device-only: "Only for this device"
do-not-use-in-production: 'As this is for development, do not use this in production.' do-not-use-in-production: 'As this is for development, do not use this in production.'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "Home" home: "Home"
local: "Local" local: "Local"
hybrid: "Social" hybrid: "Social"
hashtag: "Hashtag"
global: "Global" global: "Global"
mentions: "Mentions"
direct: "ダイレクト投稿"
notifications: "Notifications" notifications: "Notifications"
list: "Lists" list: "Lists"
swap-left: "Move to the left" swap-left: "Move to the left"
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "Local" local: "Local"
hybrid: "Social" hybrid: "Social"
global: "Global" global: "Global"
mentions: "Mentions"
messages: "Messages"
list: "Lists" list: "Lists"
hashtag: "Hashtag"
add-tag-timeline: "Add hashtag tl"
add-list: "Add list"
list-name: "List name"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "Welcome back," welcome-back: "Welcome back,"
adjective: "-san" adjective: "-san"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "Local" local: "Local"
hybrid: "Social" hybrid: "Social"
global: "Global" global: "Global"
mentions: "Mentions"
messages: "Messages"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "No posts \"{}\" found." no-posts-found: "No posts \"{}\" found."
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:

View File

@ -112,7 +112,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'Esto está en desarrollo, no usarlo para producción.' do-not-use-in-production: 'Esto está en desarrollo, no usarlo para producción.'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "Inicio" home: "Inicio"
local: "Local" local: "Local"
hybrid: "Social" hybrid: "Social"
hashtag: "ハッシュタグ"
global: "Global" global: "Global"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "Notificaciones" notifications: "Notificaciones"
list: "Listado" list: "Listado"
swap-left: "Desplazar a la izq." swap-left: "Desplazar a la izq."
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト" list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "Bienvenido/a de vuelta," welcome-back: "Bienvenido/a de vuelta,"
adjective: "-san" adjective: "-san"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:

View File

@ -112,7 +112,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "Réduire les animations dans linterface utilisateur"
this-setting-is-this-device-only: "Uniquement sur cet appareil" this-setting-is-this-device-only: "Uniquement sur cet appareil"
do-not-use-in-production: 'Il sagit dune version de développement. Ne pas utiliser dans un environnement de production.' do-not-use-in-production: 'Il sagit dune version de développement. Ne pas utiliser dans un environnement de production.'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "Accueil" home: "Accueil"
local: "Local" local: "Local"
hybrid: "Social" hybrid: "Social"
hashtag: "ハッシュタグ"
global: "Global" global: "Global"
mentions: "Mentions"
direct: "ダイレクト投稿"
notifications: "Notifications" notifications: "Notifications"
list: "Liste" list: "Liste"
swap-left: "Déplacer à gauche" swap-left: "Déplacer à gauche"
@ -259,8 +262,8 @@ common/views/components/connect-failed.troubleshooter.vue:
flush: "Vider le cache" flush: "Vider le cache"
set-version: "Choisissez une version" set-version: "Choisissez une version"
common/views/components/media-banner.vue: common/views/components/media-banner.vue:
sensitive: "閲覧注意" sensitive: "Contenu sensible"
click-to-show: "クリックして表示" click-to-show: "Cliquer pour afficher"
common/views/components/cw-button.vue: common/views/components/cw-button.vue:
hide: "Masquer" hide: "Masquer"
show: "Voir plus" show: "Voir plus"
@ -483,7 +486,7 @@ desktop/views/components/charts.vue:
drive-files-total: "ドライブのファイル数の累計" drive-files-total: "ドライブのファイル数の累計"
network-requests: "Requêtes" network-requests: "Requêtes"
network-time: "Temps de réponse" network-time: "Temps de réponse"
network-usage: "通信量" network-usage: "Traffic"
desktop/views/components/choose-file-from-drive-window.vue: desktop/views/components/choose-file-from-drive-window.vue:
choose-file: "Sélection de fichiers" choose-file: "Sélection de fichiers"
upload: "Téléverser des fichiers à partir de votre ordinateur" upload: "Téléverser des fichiers à partir de votre ordinateur"
@ -790,7 +793,7 @@ desktop/views/components/settings.profile.vue:
birthday: "Date de naissance" birthday: "Date de naissance"
save: "Mettre à jour le profil" save: "Mettre à jour le profil"
locked-account: "Protéger votre compte" locked-account: "Protéger votre compte"
is-locked: "フォローを承認制にする" is-locked: "Demande dabonnement en attente dapprobation"
other: "Autre" other: "Autre"
is-bot: "Ce compte est un Bot" is-bot: "Ce compte est un Bot"
is-cat: "Ce compte est un Chat" is-cat: "Ce compte est un Chat"
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "Local" local: "Local"
hybrid: "Social" hybrid: "Social"
global: "Global" global: "Global"
mentions: "Mentions"
messages: "メッセージ"
list: "Listes" list: "Listes"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "Content de vous revoir !" welcome-back: "Content de vous revoir !"
adjective: "さん" adjective: "さん"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "Local" local: "Local"
hybrid: "Social" hybrid: "Social"
global: "Global" global: "Global"
mentions: "Mentions"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "Pas de message avec un hashtag {} trouvé." no-posts-found: "Pas de message avec un hashtag {} trouvé."
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:
@ -1172,7 +1183,7 @@ mobile/views/pages/settings/settings.profile.vue:
avatar: "Avatar" avatar: "Avatar"
banner: "Bannière" banner: "Bannière"
is-cat: "Ce compte est un Bot" is-cat: "Ce compte est un Bot"
is-locked: "フォローを承認制にする" is-locked: "Demande dabonnement en attente dapprobation"
advanced: "Avancé" advanced: "Avancé"
privacy: "Vie privée" privacy: "Vie privée"
save: "Mettre à jour le profil" save: "Mettre à jour le profil"

View File

@ -112,7 +112,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "ホーム" home: "ホーム"
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知" notifications: "通知"
list: "リスト" list: "リスト"
swap-left: "左に移動" swap-left: "左に移動"
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト" list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、" welcome-back: "おかえりなさい、"
adjective: "さん" adjective: "さん"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:

View File

@ -119,7 +119,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
@ -166,7 +166,10 @@ common:
home: "ホーム" home: "ホーム"
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知" notifications: "通知"
list: "リスト" list: "リスト"
swap-left: "左に移動" swap-left: "左に移動"
@ -913,7 +916,13 @@ desktop/views/components/timeline.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト" list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、" welcome-back: "おかえりなさい、"
@ -1314,6 +1323,8 @@ mobile/views/pages/home.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"

View File

@ -112,7 +112,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "うち" home: "うち"
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知" notifications: "通知"
list: "リスト" list: "リスト"
swap-left: "左に移動や!" swap-left: "左に移動や!"
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト" list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "おかえり、" welcome-back: "おかえり、"
adjective: "さん" adjective: "さん"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿はあらへんで。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿はあらへんで。"
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:

View File

@ -112,7 +112,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "홈" home: "홈"
local: "로컬" local: "로컬"
hybrid: "소셜" hybrid: "소셜"
hashtag: "ハッシュタグ"
global: "글로벌" global: "글로벌"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "통지" notifications: "통지"
list: "목록" list: "목록"
swap-left: "左に移動" swap-left: "左に移動"
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト" list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、" welcome-back: "おかえりなさい、"
adjective: "さん" adjective: "さん"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:

View File

@ -112,7 +112,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "ホーム" home: "ホーム"
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知" notifications: "通知"
list: "リスト" list: "リスト"
swap-left: "左に移動" swap-left: "左に移動"
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "Lokaal" local: "Lokaal"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "Algemeen" global: "Algemeen"
mentions: "あなた宛て"
messages: "メッセージ"
list: "Lijsten" list: "Lijsten"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、" welcome-back: "おかえりなさい、"
adjective: "さん" adjective: "さん"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:

View File

@ -112,7 +112,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "ホーム" home: "ホーム"
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知" notifications: "通知"
list: "リスト" list: "リスト"
swap-left: "左に移動" swap-left: "左に移動"
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト" list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、" welcome-back: "おかえりなさい、"
adjective: "さん" adjective: "さん"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:

View File

@ -112,7 +112,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "Strona główna" home: "Strona główna"
local: "Lokalne" local: "Lokalne"
hybrid: "Społeczność" hybrid: "Społeczność"
hashtag: "ハッシュタグ"
global: "Globalne" global: "Globalne"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "Powiadomienia" notifications: "Powiadomienia"
list: "Listy" list: "Listy"
swap-left: "Przesuń w lewo" swap-left: "Przesuń w lewo"
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "Lokalne" local: "Lokalne"
hybrid: "Społeczność" hybrid: "Społeczność"
global: "Globalne" global: "Globalne"
mentions: "あなた宛て"
messages: "メッセージ"
list: "Listy" list: "Listy"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "Witaj ponownie," welcome-back: "Witaj ponownie,"
adjective: "さん" adjective: "さん"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "Lokalne" local: "Lokalne"
hybrid: "Społeczność" hybrid: "Społeczność"
global: "Globalne" global: "Globalne"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "Nie znaleziono wpisów zawierających „{}”." no-posts-found: "Nie znaleziono wpisów zawierających „{}”."
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:

View File

@ -7,12 +7,12 @@ common:
about-title: "Uma ⭐ do fediverso." about-title: "Uma ⭐ do fediverso."
about: "Obrigado por encontrar Misskey. Uma <b>plataforma descentralizada de microblog</b> nascida na Terra. Já que ela existe no Fediverso (um universo onde várias plataformas de mídia social são organizadas), ela é ligada com outras plataformas.Por que você não tira uma folga do agito e confusão da cidade, e mergulha em uma nova internet?" about: "Obrigado por encontrar Misskey. Uma <b>plataforma descentralizada de microblog</b> nascida na Terra. Já que ela existe no Fediverso (um universo onde várias plataformas de mídia social são organizadas), ela é ligada com outras plataformas.Por que você não tira uma folga do agito e confusão da cidade, e mergulha em uma nova internet?"
intro: intro:
title: "Misskeyって?" title: "O que é Misskey?"
about: "Misskeyはオープンソースの<b>分散型マイクロブログSNS</b>です。リッチで高度にカスタマイズできるUI、投稿へのリアクション、ファイルを一元管理できるドライブなど、先進的な機能を揃えています。また、Fediverseと呼ばれるネットワークに接続できるため、他のSNSともやり取りできます。例えば、あなたが何か投稿すると、その投稿はMisskeyだけでなく他のSNSにも伝わります。ちょうどある惑星から他の惑星に電波を発信している様子をイメージしてください。" about: "Misskey é um <b>serviço de microblog descentralizado</b>. Personalização sofisticada da interface, variedade de reações a posts, armazenamento de arquivos grátis com gerenciamento integrado e outras funções avançadas estão disponíveis. Um sistema em rede chamado \"Fediverso\" permite que nos comuniquemos com usuários em outras redes sociais. Se você postar algo, por exemplo, seu post não será mandado apenas para o Misskey, mas também para o Mastodon. Apenas imagine que o planeta está enviando ondas de rádio para outros planetas para se comunicar."
features: "特徴" features: "Recursos"
rich-contents: "投稿" rich-contents: "Post"
rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。" rich-contents-desc: "Apenas poste suas ideias, temas do momento e qualquer coisa que você queira compartilhar. Você pode querer decorar suas palavras, anexar suas imagens favoritas, enviar arquivos, inclusive vídeos ou criar uma enquete. Essas são as coisas que você pode fazer em Misskey."
reaction: "リアクション" reaction: "Reações"
reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。" reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
ui: "インターフェース" ui: "インターフェース"
ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。" ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。"
@ -112,7 +112,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "Início" home: "Início"
local: "Local" local: "Local"
hybrid: "Social" hybrid: "Social"
hashtag: "ハッシュタグ"
global: "Global" global: "Global"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "Notificações" notifications: "Notificações"
list: "Listas" list: "Listas"
swap-left: "Mover para a esquerda" swap-left: "Mover para a esquerda"
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト" list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、" welcome-back: "おかえりなさい、"
adjective: "さん" adjective: "さん"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:
@ -1164,23 +1175,23 @@ mobile/views/pages/games/reversi.vue:
reversi: "リバーシ" reversi: "リバーシ"
mobile/views/pages/settings/settings.profile.vue: mobile/views/pages/settings/settings.profile.vue:
title: "プロフィール" title: "プロフィール"
name: "名前" name: "Nome"
account: "アカウント" account: "Conta"
location: "場所" location: "Lugar"
description: "自己紹介" description: "Biografia"
birthday: "誕生日" birthday: "Data de nascimento"
avatar: "アイコン" avatar: "Avatar"
banner: "バナー" banner: "Capa"
is-cat: "このアカウントはCatです" is-cat: "Esta conta é gato"
is-locked: "フォローを承認制にする" is-locked: "Pedido para seguir precisa ser aprovado"
advanced: "その他" advanced: "Avançado"
privacy: "プライバシー" privacy: "Provacidade"
save: "保存" save: "Atualizar perfil"
saved: "プロフィールを保存しました" saved: "Perfil atualizado"
uploading: "アップロード中" uploading: "Enviando"
upload-failed: "アップロードに失敗しました" upload-failed: "Falha ao enviar"
mobile/views/pages/search.vue: mobile/views/pages/search.vue:
search: "検索" search: "Pesquisar"
empty: "「{}」に関する投稿は見つかりませんでした。" empty: "「{}」に関する投稿は見つかりませんでした。"
not-found: "「{}」に関する投稿は見つかりませんでした。" not-found: "「{}」に関する投稿は見つかりませんでした。"
mobile/views/pages/selectdrive.vue: mobile/views/pages/selectdrive.vue:
@ -1217,47 +1228,47 @@ mobile/views/pages/settings.vue:
load-raw-images: "添付された画像を高画質で表示する" load-raw-images: "添付された画像を高画質で表示する"
load-remote-media: "リモートサーバーのメディアを表示する" load-remote-media: "リモートサーバーのメディアを表示する"
twitter: "Twitter連携" twitter: "Twitter連携"
twitter-connect: "Twitterアカウントに接続する" twitter-connect: "Conectar à sua conta no Twitter"
twitter-reconnect: "再接続する" twitter-reconnect: "Reconectar"
twitter-disconnect: "切断する" twitter-disconnect: "Desconectar"
update: "Misskey Update" update: "Atualizar Misskey"
version: "バージョン:" version: "Versão atual;"
latest-version: "最新のバージョン:" latest-version: "Última versão:"
update-checking: "アップデートを確認中" update-checking: "Verificando atualizações"
check-for-updates: "アップデートを確認" check-for-updates: "Verificar atualizações"
no-updates: "利用可能な更新はありません" no-updates: "Sem atualizações"
no-updates-desc: "お使いのMisskeyは最新です。" no-updates-desc: "Seu Misskey está atualizado"
update-available: "新しいバージョンが利用可能です" update-available: "Uma nova versão está disponível"
update-available-desc: "ページを再度読み込みすると更新が適用されます。" update-available-desc: "Atualizações vão ser aplicadas depois de recarregar a página"
settings: "設定" settings: "Configurações"
signout: "サインアウト" signout: "Sair"
sound: "サウンド" sound: "Sons"
enable-sounds: "サウンドを有効にする" enable-sounds: "Ativar sons"
mobile/views/pages/user.vue: mobile/views/pages/user.vue:
follows-you: "フォローされています" follows-you: "Te segue"
following: "フォロー" following: "Seguindo"
followers: "フォロワー" followers: "Seguidores"
notes: "投稿" notes: "Posts"
overview: "概要" overview: "概要"
timeline: "タイムライン" timeline: "Linha do tempo"
media: "メディア" media: "Mídia"
is-suspended: "このユーザーは凍結されています。" is-suspended: "Esta conta foi suspensa"
is-remote: "Este é uma usuário remoto. O perfil que vê aqui pode não estar completo." is-remote: "Este é uma usuário remoto. O perfil que vê aqui pode não estar completo."
view-remote: "Ver o perfil completo." view-remote: "Ver o perfil completo."
mobile/views/pages/user/home.vue: mobile/views/pages/user/home.vue:
recent-notes: "Notas recentes" recent-notes: "Notas recentes"
images: "Imagens" images: "Imagens"
activity: "Atividade" activity: "Atividade"
keywords: "キーワード" keywords: "Palavras chave"
domains: "頻出ドメイン" domains: "Domínios"
frequently-replied-users: "よく会話するユーザー" frequently-replied-users: "Perguntas frequentes"
followers-you-know: "Seguidores que você conhece" followers-you-know: "Seguidores que você conhece"
last-used-at: "Ativo pela última vez:" last-used-at: "Ativo pela última vez:"
mobile/views/pages/user/home.followers-you-know.vue: mobile/views/pages/user/home.followers-you-know.vue:
loading: "Carregando" loading: "Carregando"
no-users: "知り合いのユーザーはいません" no-users: "知り合いのユーザーはいません"
mobile/views/pages/user/home.friends.vue: mobile/views/pages/user/home.friends.vue:
loading: "読み込み中" loading: "Carregando"
no-users: "よく会話するユーザーはいません" no-users: "よく会話するユーザーはいません"
mobile/views/pages/user/home.notes.vue: mobile/views/pages/user/home.notes.vue:
loading: "Carregando" loading: "Carregando"
@ -1267,14 +1278,14 @@ mobile/views/pages/user/home.photos.vue:
no-photos: "Sem fotos" no-photos: "Sem fotos"
docs: docs:
edit-this-page-on-github: "間違いや改善点を見つけましたか?" edit-this-page-on-github: "間違いや改善点を見つけましたか?"
edit-this-page-on-github-link: "このページをGitHubで編集" edit-this-page-on-github-link: "Edite esta página no GitHub!"
api: api:
entities: entities:
properties: "プロパティ" properties: "Propriedades"
endpoints: endpoints:
params: "パラメータ" params: "Parâmetros"
no-params: "パラメータはありません" no-params: "Sem parâmetros"
res: "レスポンス" res: "Resposta"
require-credential: "このエンドポイントは認証情報が必須です。" require-credential: "このエンドポイントは認証情報が必須です。"
require-permission: "このエンドポイントは{permission}の権限を必要とします。" require-permission: "このエンドポイントは{permission}の権限を必要とします。"
has-limit: "レートリミットがあります。" has-limit: "レートリミットがあります。"

View File

@ -112,7 +112,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "ホーム" home: "ホーム"
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知" notifications: "通知"
list: "リスト" list: "リスト"
swap-left: "左に移動" swap-left: "左に移動"
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト" list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、" welcome-back: "おかえりなさい、"
adjective: "さん" adjective: "さん"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:

View File

@ -112,7 +112,7 @@ common:
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
enable-animations: "アニメーションを使用" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
reversi: reversi:
@ -155,7 +155,10 @@ common:
home: "ホーム" home: "ホーム"
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知" notifications: "通知"
list: "リスト" list: "リスト"
swap-left: "左に移動" swap-left: "左に移動"
@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト" list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、" welcome-back: "おかえりなさい、"
adjective: "さん" adjective: "さん"
@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue: mobile/views/pages/welcome.vue:

View File

@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "8.42.0", "version": "8.57.1",
"clientVersion": "1.0.9769", "clientVersion": "1.0.9928",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,
@ -20,14 +20,14 @@
"format": "gulp format" "format": "gulp format"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome": "1.1.8", "@fortawesome/fontawesome-svg-core": "1.2.4",
"@fortawesome/fontawesome-free-brands": "5.0.13", "@fortawesome/free-brands-svg-icons": "5.3.1",
"@fortawesome/fontawesome-free-regular": "5.0.13", "@fortawesome/free-regular-svg-icons": "5.3.1",
"@fortawesome/fontawesome-free-solid": "5.0.13", "@fortawesome/free-solid-svg-icons": "5.3.1",
"@koa/cors": "2.2.2", "@koa/cors": "2.2.2",
"@prezzemolo/rap": "0.1.2", "@prezzemolo/rap": "0.1.2",
"@prezzemolo/zip": "0.0.3", "@prezzemolo/zip": "0.0.3",
"@types/bcryptjs": "2.4.1", "@types/bcryptjs": "2.4.2",
"@types/dateformat": "1.0.1", "@types/dateformat": "1.0.1",
"@types/debug": "0.0.30", "@types/debug": "0.0.30",
"@types/deep-equal": "1.0.1", "@types/deep-equal": "1.0.1",
@ -51,7 +51,7 @@
"@types/koa-logger": "3.1.0", "@types/koa-logger": "3.1.0",
"@types/koa-mount": "3.0.1", "@types/koa-mount": "3.0.1",
"@types/koa-multer": "1.0.0", "@types/koa-multer": "1.0.0",
"@types/koa-router": "7.0.31", "@types/koa-router": "7.0.32",
"@types/koa-send": "4.1.1", "@types/koa-send": "4.1.1",
"@types/koa-views": "2.0.3", "@types/koa-views": "2.0.3",
"@types/koa__cors": "2.2.3", "@types/koa__cors": "2.2.3",
@ -60,7 +60,7 @@
"@types/mocha": "5.2.3", "@types/mocha": "5.2.3",
"@types/mongodb": "3.1.7", "@types/mongodb": "3.1.7",
"@types/ms": "0.7.30", "@types/ms": "0.7.30",
"@types/node": "10.9.4", "@types/node": "10.10.1",
"@types/portscanner": "2.1.0", "@types/portscanner": "2.1.0",
"@types/pug": "2.0.4", "@types/pug": "2.0.4",
"@types/qrcode": "1.2.0", "@types/qrcode": "1.2.0",
@ -77,7 +77,7 @@
"@types/systeminformation": "3.23.0", "@types/systeminformation": "3.23.0",
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",
"@types/uuid": "3.4.4", "@types/uuid": "3.4.4",
"@types/webpack": "4.4.11", "@types/webpack": "4.4.12",
"@types/webpack-stream": "3.2.10", "@types/webpack-stream": "3.2.10",
"@types/websocket": "0.0.40", "@types/websocket": "0.0.40",
"@types/ws": "6.0.1", "@types/ws": "6.0.1",
@ -217,11 +217,11 @@
"vuewordcloud": "18.7.11", "vuewordcloud": "18.7.11",
"vuex": "3.0.1", "vuex": "3.0.1",
"vuex-persistedstate": "2.5.4", "vuex-persistedstate": "2.5.4",
"web-push": "3.3.2", "web-push": "3.3.3",
"webfinger.js": "2.6.6", "webfinger.js": "2.6.6",
"webpack": "4.19.0", "webpack": "4.19.1",
"webpack-cli": "3.1.0", "webpack-cli": "3.1.0",
"websocket": "1.0.26", "websocket": "1.0.28",
"ws": "6.0.0", "ws": "6.0.0",
"xev": "2.0.1" "xev": "2.0.1"
}, },

View File

@ -1,3 +1,24 @@
<template> <template>
<router-view id="app"></router-view> <router-view id="app" v-hotkey.global="keymap"></router-view>
</template> </template>
<script lang="ts">
import Vue from 'vue';
import { url, lang } from './config';
export default Vue.extend({
computed: {
keymap(): any {
return {
'h|slash': this.help
};
}
},
methods: {
help() {
window.open(`${url}/docs/${lang}/keyboard-shortcut`, '_blank');
}
}
});
</script>

View File

@ -0,0 +1,109 @@
import keyCode from './keycode';
import { concat } from '../../../prelude/array';
type pattern = {
which: string[];
ctrl?: boolean;
shift?: boolean;
alt?: boolean;
};
type action = {
patterns: pattern[];
callback: Function;
};
const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => {
const result = {
patterns: [],
callback: callback
} as action;
result.patterns = patterns.split('|').map(part => {
const pattern = {
which: [],
ctrl: false,
alt: false,
shift: false
} as pattern;
part.trim().split('+').forEach(key => {
key = key.trim().toLowerCase();
switch (key) {
case 'ctrl': pattern.ctrl = true; break;
case 'alt': pattern.alt = true; break;
case 'shift': pattern.shift = true; break;
default: pattern.which = keyCode(key).map(k => k.toLowerCase());
}
});
return pattern;
});
return result;
});
const ignoreElemens = ['input', 'textarea'];
export default {
install(Vue) {
Vue.directive('hotkey', {
bind(el, binding) {
el._hotkey_global = binding.modifiers.global === true;
const actions = getKeyMap(binding.value);
// flatten
const reservedKeys = concat(concat(actions.map(a => a.patterns.map(p => p.which))));
el.dataset.reservedKeys = reservedKeys.map(key => `'${key}'`).join(' ');
el._keyHandler = e => {
const key = e.code.toLowerCase();
const targetReservedKeys = document.activeElement ? ((document.activeElement as any).dataset || {}).reservedKeys || '' : '';
if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
for (const action of actions) {
if (el._hotkey_global && targetReservedKeys.includes(`'${key}'`)) break;
const matched = action.patterns.some(pattern => {
const matched = pattern.which.includes(key) &&
pattern.ctrl == e.ctrlKey &&
pattern.shift == e.shiftKey &&
pattern.alt == e.altKey;
if (matched) {
e.preventDefault();
e.stopPropagation();
action.callback(e);
return true;
} else {
return false;
}
});
if (matched) {
break;
}
}
};
if (el._hotkey_global) {
document.addEventListener('keydown', el._keyHandler);
} else {
el.addEventListener('keydown', el._keyHandler);
}
},
unbind(el) {
if (el._hotkey_global) {
document.removeEventListener('keydown', el._keyHandler);
} else {
el.removeEventListener('keydown', el._keyHandler);
}
}
});
}
};

View File

@ -0,0 +1,33 @@
export default (input: string): string[] => {
if (Object.keys(aliases).some(a => a.toLowerCase() == input.toLowerCase())) {
const codes = aliases[input];
return Array.isArray(codes) ? codes : [codes];
} else {
return [input];
}
};
export const aliases = {
'esc': 'Escape',
'enter': ['Enter', 'NumpadEnter'],
'up': 'ArrowUp',
'down': 'ArrowDown',
'left': 'ArrowLeft',
'right': 'ArrowRight',
'plus': ['NumpadAdd', 'Semicolon'],
};
/*!
* Programatically add the following
*/
// lower case chars
for (let i = 97; i < 123; i++) {
const char = String.fromCharCode(i);
aliases[char] = `Key${char.toUpperCase()}`;
}
// numbers
for (let i = 0; i < 10; i++) {
aliases[i] = [`Numpad${i}`, `Digit${i}`];
}

View File

@ -0,0 +1,13 @@
import Stream from './stream';
import MiOS from '../../../mios';
export class HashtagStream extends Stream {
constructor(os: MiOS, me, q) {
super(os, 'hashtag', me ? {
i: me.token,
q: JSON.stringify(q)
} : {
q: JSON.stringify(q)
});
}
}

View File

@ -50,6 +50,30 @@ export class HomeStream extends Stream {
}); });
}); });
this.on('unreadMention', () => {
os.store.dispatch('mergeMe', {
hasUnreadMentions: true
});
});
this.on('readAllUnreadMentions', () => {
os.store.dispatch('mergeMe', {
hasUnreadMentions: false
});
});
this.on('unreadSpecifiedNote', () => {
os.store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: true
});
});
this.on('readAllUnreadSpecifiedNotes', () => {
os.store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: false
});
});
this.on('clientSettingUpdated', x => { this.on('clientSettingUpdated', x => {
os.store.commit('settings/set', { os.store.commit('settings/set', {
key: x.key, key: x.key,

View File

@ -2,9 +2,9 @@
<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv"> <div class="onchrpzrvnoruiaenfcqvccjfuupzzwv">
<div class="backdrop" ref="backdrop" @click="close"></div> <div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ hukidasi }" ref="popover"> <div class="popover" :class="{ hukidasi }" ref="popover">
<template v-for="item in items"> <template v-for="item, i in items">
<div v-if="item === null"></div> <div v-if="item === null"></div>
<button v-if="item" @click="clicked(item.action)" v-html="item.icon ? item.icon + ' ' + item.text : item.text"></button> <button v-if="item" @click="clicked(item.action)" v-html="item.icon ? item.icon + ' ' + item.text : item.text" :tabindex="i"></button>
</template> </template>
</div> </div>
</div> </div>

View File

@ -33,12 +33,16 @@ export default Vue.extend({
text: '%i18n:@pin%', text: '%i18n:@pin%',
action: this.pin action: this.pin
}); });
}
if (this.note.userId == this.$store.state.i.id || this.$store.state.i.isAdmin) {
items.push({ items.push({
icon: '%fa:trash-alt R%', icon: '%fa:trash-alt R%',
text: '%i18n:@delete%', text: '%i18n:@delete%',
action: this.del action: this.del
}); });
} }
if (this.note.uri) { if (this.note.uri) {
items.push({ items.push({
icon: '%fa:external-link-square-alt%', icon: '%fa:external-link-square-alt%',
@ -48,6 +52,7 @@ export default Vue.extend({
} }
}); });
} }
return items; return items;
} }
}, },

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="mk-reaction-picker"> <div class="mk-reaction-picker" v-hotkey.global="keymap">
<div class="backdrop" ref="backdrop" @click="close"></div> <div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ compact, big }" ref="popover"> <div class="popover" :class="{ compact, big }" ref="popover">
<p v-if="!compact">{{ title }}</p> <p v-if="!compact">{{ title }}</p>
<div> <div ref="buttons" :class="{ showFocus }">
<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button> <button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button> <button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button>
<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button> <button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button>
@ -31,30 +31,84 @@ export default Vue.extend({
type: Object, type: Object,
required: true required: true
}, },
source: { source: {
required: true required: true
}, },
compact: { compact: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
}, },
cb: { cb: {
required: false required: false
}, },
big: { big: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
},
showFocus: {
type: Boolean,
required: false,
default: false
},
animation: {
type: Boolean,
required: false,
default: true
} }
}, },
data() { data() {
return { return {
title: placeholder title: placeholder,
focus: null
}; };
}, },
computed: {
keymap(): any {
return {
'esc': this.close,
'enter|space|plus': this.choose,
'up|k': this.focusUp,
'left|h|shift+tab': this.focusLeft,
'right|l|tab': this.focusRight,
'down|j': this.focusDown,
'1': () => this.react('like'),
'2': () => this.react('love'),
'3': () => this.react('laugh'),
'4': () => this.react('hmm'),
'5': () => this.react('surprise'),
'6': () => this.react('congrats'),
'7': () => this.react('angry'),
'8': () => this.react('confused'),
'9': () => this.react('rip'),
'0': () => this.react('pudding'),
};
}
},
watch: {
focus(i) {
this.$refs.buttons.children[i].focus();
if (this.showFocus) {
this.title = this.$refs.buttons.children[i].title;
}
}
},
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
this.focus = 0;
const popover = this.$refs.popover as any; const popover = this.$refs.popover as any;
const rect = this.source.getBoundingClientRect(); const rect = this.source.getBoundingClientRect();
@ -76,7 +130,7 @@ export default Vue.extend({
anime({ anime({
targets: this.$refs.backdrop, targets: this.$refs.backdrop,
opacity: 1, opacity: 1,
duration: 100, duration: this.animation ? 100 : 0,
easing: 'linear' easing: 'linear'
}); });
@ -84,10 +138,11 @@ export default Vue.extend({
targets: this.$refs.popover, targets: this.$refs.popover,
opacity: 1, opacity: 1,
scale: [0.5, 1], scale: [0.5, 1],
duration: 500 duration: this.animation ? 500 : 0
}); });
}); });
}, },
methods: { methods: {
react(reaction) { react(reaction) {
(this as any).api('notes/reactions/create', { (this as any).api('notes/reactions/create', {
@ -95,21 +150,25 @@ export default Vue.extend({
reaction: reaction reaction: reaction
}).then(() => { }).then(() => {
if (this.cb) this.cb(); if (this.cb) this.cb();
this.$emit('closed');
this.destroyDom(); this.destroyDom();
}); });
}, },
onMouseover(e) { onMouseover(e) {
this.title = e.target.title; this.title = e.target.title;
}, },
onMouseout(e) { onMouseout(e) {
this.title = placeholder; this.title = placeholder;
}, },
close() { close() {
(this.$refs.backdrop as any).style.pointerEvents = 'none'; (this.$refs.backdrop as any).style.pointerEvents = 'none';
anime({ anime({
targets: this.$refs.backdrop, targets: this.$refs.backdrop,
opacity: 0, opacity: 0,
duration: 200, duration: this.animation ? 200 : 0,
easing: 'linear' easing: 'linear'
}); });
@ -118,10 +177,33 @@ export default Vue.extend({
targets: this.$refs.popover, targets: this.$refs.popover,
opacity: 0, opacity: 0,
scale: 0.5, scale: 0.5,
duration: 200, duration: this.animation ? 200 : 0,
easing: 'easeInBack', easing: 'easeInBack',
complete: () => this.destroyDom() complete: () => {
this.$emit('closed');
this.destroyDom();
}
}); });
},
focusUp() {
this.focus = this.focus == 0 ? 9 : this.focus < 5 ? (this.focus + 4) : (this.focus - 5);
},
focusDown() {
this.focus = this.focus == 9 ? 0 : this.focus >= 5 ? (this.focus - 4) : (this.focus + 5);
},
focusRight() {
this.focus = this.focus == 9 ? 0 : (this.focus + 1);
},
focusLeft() {
this.focus = this.focus == 0 ? 9 : (this.focus - 1);
},
choose() {
this.$refs.buttons.childNodes[this.focus].click();
} }
} }
}); });
@ -207,6 +289,21 @@ root(isDark)
width 240px width 240px
text-align center text-align center
&.showFocus
> button:focus
z-index 1
&:after
content ""
pointer-events none
position absolute
top 0
right 0
bottom 0
left 0
border 2px solid rgba($theme-color, 0.3)
border-radius 4px
> button > button
padding 0 padding 0
width 40px width 40px

View File

@ -100,7 +100,7 @@ export default Vue.extend({
created() { created() {
(this as any).api('chart', { (this as any).api('chart', {
limit: 32 limit: 35
}).then(chart => { }).then(chart => {
this.chart = chart; this.chart = chart;
}); });
@ -681,6 +681,6 @@ export default Vue.extend({
> div > div
> * > *
display block display block
height 320px height 350px
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy"> <mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
<span slot="header"> <span slot="header">
<span v-html="title" :class="$style.title"></span> <span v-html="title" :class="$style.title"></span>
<span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}%i18n:@choose-file%)</span> <span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}%i18n:@choose-file%)</span>

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy"> <mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
<span slot="header"> <span slot="header">
<span v-html="title" :class="$style.title"></span> <span v-html="title" :class="$style.title"></span>
</span> </span>

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout-url="popout"> <mk-window ref="window" @closed="destroyDom" width="800px" height="500px" :popout-url="popout">
<template slot="header"> <template slot="header">
<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:@used%</p> <p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:@used%</p>
<span :class="$style.title">%fa:cloud%%i18n:@drive%</span> <span :class="$style.title">%fa:cloud%%i18n:@drive%</span>

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window width="400px" height="550px" @closed="$destroy"> <mk-window width="400px" height="550px" @closed="destroyDom">
<span slot="header" :class="$style.header"> <span slot="header" :class="$style.header">
<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }} <img :src="user.avatarUrl" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }}
</span> </span>

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window width="400px" height="550px" @closed="$destroy"> <mk-window width="400px" height="550px" @closed="destroyDom">
<span slot="header" :class="$style.header"> <span slot="header" :class="$style.header">
<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }} <img :src="user.avatarUrl" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }}
</span> </span>

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy"> <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
<span slot="header" :class="$style.header">%fa:gamepad%%i18n:@game%</span> <span slot="header" :class="$style.header">%fa:gamepad%%i18n:@game%</span>
<mk-reversi :class="$style.content" @gamed="g => game = g"/> <mk-reversi :class="$style.content" @gamed="g => game = g"/>
</mk-window> </mk-window>

View File

@ -38,7 +38,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="main"> <div class="main" :class="{ withBg: $store.state.i.wallpaperUrl != null }">
<template v-if="customize"> <template v-if="customize">
<x-draggable v-for="place in ['left', 'right']" <x-draggable v-for="place in ['left', 'right']"
:list="widgets[place]" :list="widgets[place]"
@ -237,6 +237,10 @@ export default Vue.extend({
warp(date) { warp(date) {
(this.$refs.tl as any).warp(date); (this.$refs.tl as any).warp(date);
},
focus() {
(this.$refs.tl as any).focus();
} }
} }
}); });
@ -336,7 +340,10 @@ root(isDark)
display flex display flex
justify-content center justify-content center
margin 0 auto margin 0 auto
max-width 1220px max-width 1240px
&.withBg
background rgba(isDark ? #000 : #fff, 0.5)
> * > *
.customize-container .customize-container
@ -351,7 +358,7 @@ root(isDark)
> .main > .main
padding 16px padding 16px
width calc(100% - 275px * 2) width calc(100% - 280px * 2)
order 2 order 2
> .form > .form
@ -367,7 +374,7 @@ root(isDark)
border-radius 0 border-radius 0
> *:not(.main) > *:not(.main)
width 275px width 280px
padding 16px 0 16px 0 padding 16px 0 16px 0
> *:not(:last-child) > *:not(:last-child)

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="$destroy"> <mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="destroyDom">
<span slot="header" :class="$style.header"> <span slot="header" :class="$style.header">
%fa:i-cursor%{{ title }} %fa:i-cursor%{{ title }}
</span> </span>

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy"> <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
<span slot="header" :class="$style.header">%fa:comments%%i18n:@title% {{ user | userName }}</span> <span slot="header" :class="$style.header">%fa:comments%%i18n:@title% {{ user | userName }}</span>
<mk-messaging-room :user="user" :class="$style.content"/> <mk-messaging-room :user="user" :class="$style.content"/>
</mk-window> </mk-window>

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window ref="window" width="500px" height="560px" @closed="$destroy"> <mk-window ref="window" width="500px" height="560px" @closed="destroyDom">
<span slot="header" :class="$style.header">%fa:comments%%i18n:@title%</span> <span slot="header" :class="$style.header">%fa:comments%%i18n:@title%</span>
<mk-messaging :class="$style.content" @navigate="navigate"/> <mk-messaging :class="$style.content" @navigate="navigate"/>
</mk-window> </mk-window>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="note" tabindex="-1" :title="title" @keydown="onKeydown"> <div class="note" tabindex="-1" v-hotkey="keymap" :title="title">
<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> <div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
<x-sub :note="p.reply"/> <x-sub :note="p.reply"/>
</div> </div>
@ -40,18 +40,18 @@
</div> </div>
<footer> <footer>
<mk-reactions-viewer :note="p" ref="reactionsViewer"/> <mk-reactions-viewer :note="p" ref="reactionsViewer"/>
<button class="replyButton" @click="reply" title="%i18n:@reply%"> <button class="replyButton" @click="reply()" title="%i18n:@reply%">
<template v-if="p.reply">%fa:reply-all%</template> <template v-if="p.reply">%fa:reply-all%</template>
<template v-else>%fa:reply%</template> <template v-else>%fa:reply%</template>
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> <p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
</button> </button>
<button class="renoteButton" @click="renote" title="%i18n:@renote%"> <button class="renoteButton" @click="renote()" title="%i18n:@renote%">
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
</button> </button>
<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@add-reaction%"> <button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react()" ref="reactButton" title="%i18n:@add-reaction%">
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
</button> </button>
<button @click="menu" ref="menuButton"> <button @click="menu()" ref="menuButton">
%fa:ellipsis-h% %fa:ellipsis-h%
</button> </button>
<!-- <button title="%i18n:@detail"> <!-- <button title="%i18n:@detail">
@ -111,6 +111,30 @@ export default Vue.extend({
}, },
computed: { computed: {
keymap(): any {
return {
'r|left': () => this.reply(true),
'e|a|plus': () => this.react(true),
'q|right': () => this.renote(true),
'ctrl+q|ctrl+right': this.renoteDirectly,
'up|k|shift+tab': this.focusBefore,
'down|j|tab': this.focusAfter,
'esc': this.blur,
'm|o': () => this.menu(true),
's': this.toggleShowContent,
'1': () => this.reactDirectly('like'),
'2': () => this.reactDirectly('love'),
'3': () => this.reactDirectly('laugh'),
'4': () => this.reactDirectly('hmm'),
'5': () => this.reactDirectly('surprise'),
'6': () => this.reactDirectly('congrats'),
'7': () => this.reactDirectly('angry'),
'8': () => this.reactDirectly('confused'),
'9': () => this.reactDirectly('rip'),
'0': () => this.reactDirectly('pudding'),
};
},
isRenote(): boolean { isRenote(): boolean {
return (this.note.renote && return (this.note.renote &&
this.note.text == null && this.note.text == null &&
@ -189,10 +213,14 @@ export default Vue.extend({
methods: { methods: {
capture(withHandler = false) { capture(withHandler = false) {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection.send({ const data = {
type: 'capture', type: 'capture',
id: this.p.id id: this.p.id
}); } as any;
if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
data.read = true;
}
this.connection.send(data);
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
} }
}, },
@ -220,67 +248,69 @@ export default Vue.extend({
} }
}, },
reply() { reply(viaKeyboard = false) {
(this as any).os.new(MkPostFormWindow, { (this as any).os.new(MkPostFormWindow, {
reply: this.p reply: this.p,
}); animation: !viaKeyboard
}).$once('closed', this.focus);
}, },
renote() { renote(viaKeyboard = false) {
(this as any).os.new(MkRenoteFormWindow, { (this as any).os.new(MkRenoteFormWindow, {
note: this.p note: this.p,
animation: !viaKeyboard
}).$once('closed', this.focus);
},
renoteDirectly() {
(this as any).api('notes/create', {
renoteId: this.p.id
}); });
}, },
react() { react(viaKeyboard = false) {
this.blur();
(this as any).os.new(MkReactionPicker, { (this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton, source: this.$refs.reactButton,
note: this.p note: this.p,
showFocus: viaKeyboard,
animation: !viaKeyboard
}).$once('closed', this.focus);
},
reactDirectly(reaction) {
(this as any).api('notes/reactions/create', {
noteId: this.p.id,
reaction: reaction
}); });
}, },
menu() { menu(viaKeyboard = false) {
(this as any).os.new(MkNoteMenu, { (this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton, source: this.$refs.menuButton,
note: this.p note: this.p,
}); animation: !viaKeyboard
}).$once('closed', this.focus);
}, },
onKeydown(e) { toggleShowContent() {
let shouldBeCancel = true; this.showContent = !this.showContent;
},
switch (true) { focus() {
case e.which == 38: // [↑] this.$el.focus();
case e.which == 74: // [j] },
case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
focus(this.$el, e => e.previousElementSibling);
break;
case e.which == 40: // [↓] blur() {
case e.which == 75: // [k] this.$el.blur();
case e.which == 9: // [Tab] },
focus(this.$el, e => e.nextElementSibling);
break;
case e.which == 81: // [q] focusBefore() {
case e.which == 69: // [e] focus(this.$el, e => e.previousElementSibling);
this.renote(); },
break;
case e.which == 70: // [f] focusAfter() {
case e.which == 76: // [l] focus(this.$el, e => e.nextElementSibling);
//this.like();
break;
case e.which == 82: // [r]
this.reply();
break;
default:
shouldBeCancel = false;
}
if (shouldBeCancel) e.preventDefault();
} }
} }
}); });

View File

@ -10,9 +10,9 @@
</div> </div>
<!-- トランジションを有効にするとなぜかメモリリークする --> <!-- トランジションを有効にするとなぜかメモリリークする -->
<component :is="$store.state.device.animations ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div"> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div" ref="notes">
<template v-for="(note, i) in _notes"> <template v-for="(note, i) in _notes">
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" ref="note"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
<span>%fa:angle-up%{{ note._datetext }}</span> <span>%fa:angle-up%{{ note._datetext }}</span>
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
@ -89,7 +89,7 @@ export default Vue.extend({
}, },
focus() { focus() {
(this.$el as any).children[0].focus(); (this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus();
}, },
onNoteUpdated(i, note) { onNoteUpdated(i, note) {

View File

@ -2,7 +2,7 @@
<div class="mk-notifications"> <div class="mk-notifications">
<div class="notifications" v-if="notifications.length != 0"> <div class="notifications" v-if="notifications.length != 0">
<!-- トランジションを有効にするとなぜかメモリリークする --> <!-- トランジションを有効にするとなぜかメモリリークする -->
<component :is="$store.state.device.animations ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div"> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div">
<template v-for="(notification, i) in _notifications"> <template v-for="(notification, i) in _notifications">
<div class="notification" :class="notification.type" :key="notification.id"> <div class="notification" :class="notification.type" :key="notification.id">
<mk-time :time="notification.createdAt"/> <mk-time :time="notification.createdAt"/>

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window class="mk-post-form-window" ref="window" is-modal @closed="$destroy"> <mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed" :animation="animation">
<span slot="header" class="mk-post-form-window--header"> <span slot="header" class="mk-post-form-window--header">
<span class="icon" v-if="geo">%fa:map-marker-alt%</span> <span class="icon" v-if="geo">%fa:map-marker-alt%</span>
<span v-if="!reply">%i18n:@note%</span> <span v-if="!reply">%i18n:@note%</span>
@ -25,7 +25,19 @@
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
props: ['reply'], props: {
reply: {
type: Object,
required: false
},
animation: {
type: Boolean,
required: false,
default: true
}
},
data() { data() {
return { return {
uploadings: [], uploadings: [],
@ -33,11 +45,13 @@ export default Vue.extend({
geo: null geo: null
}; };
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
(this.$refs.form as any).focus(); (this.$refs.form as any).focus();
}); });
}, },
methods: { methods: {
onChangeUploadings(files) { onChangeUploadings(files) {
this.uploadings = files; this.uploadings = files;
@ -53,6 +67,10 @@ export default Vue.extend({
}, },
onPosted() { onPosted() {
(this.$refs.window as any).close(); (this.$refs.window as any).close();
},
onWindowClosed() {
this.$emit('closed');
this.destroyDom();
} }
} }
}); });

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="$destroy"> <mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="destroyDom">
<span slot="header">{{ title }}<mk-ellipsis/></span> <span slot="header">{{ title }}<mk-ellipsis/></span>
<div :class="$style.body"> <div :class="$style.body">
<p :class="$style.init" v-if="isNaN(value)">%i18n:@waiting%<mk-ellipsis/></p> <p :class="$style.init" v-if="isNaN(value)">%i18n:@waiting%<mk-ellipsis/></p>

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy"> <mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
<span slot="header">%fa:envelope R% %i18n:@title%</span> <span slot="header">%fa:envelope R% %i18n:@title%</span>
<div class="slpqaxdoxhvglersgjukmvizkqbmbokc" :data-darkmode="$store.state.device.darkmode"> <div class="slpqaxdoxhvglersgjukmvizkqbmbokc" :data-darkmode="$store.state.device.darkmode">

View File

@ -1,7 +1,7 @@
<template> <template>
<mk-window ref="window" is-modal @closed="$destroy"> <mk-window ref="window" is-modal @closed="onWindowClosed" :animation="animation">
<span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span> <span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span>
<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/> <mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/>
</mk-window> </mk-window>
</template> </template>
@ -9,26 +9,48 @@
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
props: ['note'], props: {
mounted() { note: {
document.addEventListener('keydown', this.onDocumentKeydown); type: Object,
required: true
},
animation: {
type: Boolean,
required: false,
default: true
}
}, },
beforeDestroy() {
document.removeEventListener('keydown', this.onDocumentKeydown); computed: {
keymap(): any {
return {
'esc': this.close,
'enter': this.post,
'q': this.quote,
};
}
}, },
methods: { methods: {
onDocumentKeydown(e) { post() {
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { (this.$refs.form as any).ok();
if (e.which == 27) { // Esc },
(this.$refs.window as any).close(); quote() {
} (this.$refs.form as any).onQuote();
} },
close() {
(this.$refs.window as any).close();
}, },
onPosted() { onPosted() {
(this.$refs.window as any).close(); (this.$refs.window as any).close();
}, },
onCanceled() { onCanceled() {
(this.$refs.window as any).close(); (this.$refs.window as any).close();
},
onWindowClosed() {
this.$emit('closed');
this.destroyDom();
} }
} }
}); });

View File

@ -1,13 +1,19 @@
<template> <template>
<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy"> <mk-window ref="window" is-modal width="700px" height="550px" @closed="destroyDom">
<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span> <span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span>
<mk-settings @done="close"/> <mk-settings :initial-page="initialPage" @done="close"/>
</mk-window> </mk-window>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
props: {
initialPage: {
type: String,
required: false
}
},
methods: { methods: {
close() { close() {
(this as any).$refs.window.close(); (this as any).$refs.window.close();

View File

@ -0,0 +1,65 @@
<template>
<div class="vfcitkilproprqtbnpoertpsziierwzi">
<div v-for="timeline in timelines" class="timeline">
<ui-input v-model="timeline.title" @change="save">
<span>%i18n:@title%</span>
</ui-input>
<ui-textarea :value="timeline.query ? timeline.query.map(tags => tags.join(' ')).join('\n') : ''" @input="onQueryChange(timeline, $event)">
<span>%i18n:@query%</span>
</ui-textarea>
<ui-button class="save" @click="save">%i18n:@save%</ui-button>
</div>
<ui-button class="add" @click="add">%i18n:@add%</ui-button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import * as uuid from 'uuid';
export default Vue.extend({
data() {
return {
timelines: this.$store.state.settings.tagTimelines
};
},
methods: {
add() {
this.timelines.push({
id: uuid(),
title: '',
query: ''
});
this.save();
},
save() {
this.$store.dispatch('settings/set', { key: 'tagTimelines', value: this.timelines });
},
onQueryChange(timeline, value) {
timeline.query = value.split('\n').map(tags => tags.split(' '));
}
}
});
</script>
<style lang="stylus" scoped>
root(isDark)
> .timeline
padding-bottom 16px
border-bottom solid 1px rgba(#000, 0.1)
> .add
margin-top 16px
.vfcitkilproprqtbnpoertpsziierwzi[data-darkmode]
root(true)
.vfcitkilproprqtbnpoertpsziierwzi:not([data-darkmode])
root(false)
</style>

View File

@ -5,6 +5,7 @@
<p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p> <p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p>
<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p> <p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p>
<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p> <p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p>
<p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'">%fa:hashtag .fw%%i18n:@tags%</p>
<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p> <p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p>
<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p> <p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p>
<p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p> <p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p>
@ -60,7 +61,7 @@
<button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button> <button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button>
<mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/> <mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/>
<mk-switch v-model="circleIcons" text="%i18n:@circle-icons%"/> <mk-switch v-model="circleIcons" text="%i18n:@circle-icons%"/>
<mk-switch v-model="animations" text="%i18n:common.enable-animations%"/> <mk-switch v-model="reduceMotion" text="%i18n:common.reduce-motion%"/>
<mk-switch v-model="contrastedAcct" text="%i18n:@contrasted-acct%"/> <mk-switch v-model="contrastedAcct" text="%i18n:@contrasted-acct%"/>
<mk-switch v-model="showFullAcct" text="%i18n:common.show-full-acct%"/> <mk-switch v-model="showFullAcct" text="%i18n:common.show-full-acct%"/>
<mk-switch v-model="gradientWindowHeader" text="%i18n:@gradient-window-header%"/> <mk-switch v-model="gradientWindowHeader" text="%i18n:@gradient-window-header%"/>
@ -138,6 +139,11 @@
<x-drive/> <x-drive/>
</section> </section>
<section class="hashtags" v-show="page == 'hashtags'">
<h1>%i18n:@tags%</h1>
<x-tags/>
</section>
<section class="mute" v-show="page == 'mute'"> <section class="mute" v-show="page == 'mute'">
<h1>%i18n:@mute%</h1> <h1>%i18n:@mute%</h1>
<x-mute/> <x-mute/>
@ -222,6 +228,7 @@ import XApi from './settings.api.vue';
import XApps from './settings.apps.vue'; import XApps from './settings.apps.vue';
import XSignins from './settings.signins.vue'; import XSignins from './settings.signins.vue';
import XDrive from './settings.drive.vue'; import XDrive from './settings.drive.vue';
import XTags from './settings.tags.vue';
import { url, langs, version } from '../../../config'; import { url, langs, version } from '../../../config';
import checkForUpdate from '../../../common/scripts/check-for-update'; import checkForUpdate from '../../../common/scripts/check-for-update';
@ -234,11 +241,18 @@ export default Vue.extend({
XApi, XApi,
XApps, XApps,
XSignins, XSignins,
XDrive XDrive,
XTags
},
props: {
initialPage: {
type: String,
required: false
}
}, },
data() { data() {
return { return {
page: 'profile', page: this.initialPage || 'profile',
meta: null, meta: null,
version, version,
langs, langs,
@ -247,9 +261,9 @@ export default Vue.extend({
}; };
}, },
computed: { computed: {
animations: { reduceMotion: {
get() { return this.$store.state.device.animations; }, get() { return this.$store.state.device.reduceMotion; },
set(value) { this.$store.commit('device/set', { key: 'animations', value }); } set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
}, },
apiViaStream: { apiViaStream: {

View File

@ -15,6 +15,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
const fetchLimit = 10; const fetchLimit = 10;
@ -23,6 +24,9 @@ export default Vue.extend({
src: { src: {
type: String, type: String,
required: true required: true
},
tagTl: {
required: false
} }
}, },
@ -31,9 +35,17 @@ export default Vue.extend({
fetching: true, fetching: true,
moreFetching: false, moreFetching: false,
existMore: false, existMore: false,
streamManager: null,
connection: null, connection: null,
connectionId: null, connectionId: null,
date: null date: null,
baseQuery: {
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
},
query: {},
endpoint: null
}; };
}, },
@ -42,53 +54,109 @@ export default Vue.extend({
return this.$store.state.i.followingCount == 0; return this.$store.state.i.followingCount == 0;
}, },
stream(): any {
switch (this.src) {
case 'home': return (this as any).os.stream;
case 'local': return (this as any).os.streams.localTimelineStream;
case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
case 'global': return (this as any).os.streams.globalTimelineStream;
}
},
endpoint(): string {
switch (this.src) {
case 'home': return 'notes/timeline';
case 'local': return 'notes/local-timeline';
case 'hybrid': return 'notes/hybrid-timeline';
case 'global': return 'notes/global-timeline';
}
},
canFetchMore(): boolean { canFetchMore(): boolean {
return !this.moreFetching && !this.fetching && this.existMore; return !this.moreFetching && !this.fetching && this.existMore;
} }
}, },
mounted() { mounted() {
this.connection = this.stream.getConnection(); const prepend = note => {
this.connectionId = this.stream.use(); (this.$refs.timeline as any).prepend(note);
};
this.connection.on('note', this.onNote); if (this.src == 'tag') {
if (this.src == 'home') { this.endpoint = 'notes/search_by_tag';
this.connection.on('follow', this.onChangeFollowing); this.query = {
this.connection.on('unfollow', this.onChangeFollowing); query: this.tagTl.query
};
this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.connection.close();
});
} else if (this.src == 'home') {
this.endpoint = 'notes/timeline';
const onChangeFollowing = () => {
this.fetch();
};
this.streamManager = (this as any).os.stream;
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend);
this.connection.on('follow', onChangeFollowing);
this.connection.on('unfollow', onChangeFollowing);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.connection.off('follow', onChangeFollowing);
this.connection.off('unfollow', onChangeFollowing);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'local') {
this.endpoint = 'notes/local-timeline';
this.streamManager = (this as any).os.streams.localTimelineStream;
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'hybrid') {
this.endpoint = 'notes/hybrid-timeline';
this.streamManager = (this as any).os.streams.hybridTimelineStream;
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'global') {
this.endpoint = 'notes/global-timeline';
this.streamManager = (this as any).os.streams.globalTimelineStream;
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'mentions') {
this.endpoint = 'notes/mentions';
this.streamManager = (this as any).os.stream;
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('mention', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('mention', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'messages') {
this.endpoint = 'notes/mentions';
this.query = {
visibility: 'specified'
};
const onNote = note => {
if (note.visibility == 'specified') {
prepend(note);
}
};
this.streamManager = (this as any).os.stream;
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('mention', onNote);
this.$once('beforeDestroy', () => {
this.connection.off('mention', onNote);
this.streamManager.dispose(this.connectionId);
});
} }
document.addEventListener('keydown', this.onKeydown);
this.fetch(); this.fetch();
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('note', this.onNote); this.$emit('beforeDestroy');
if (this.src == 'home') {
this.connection.off('follow', this.onChangeFollowing);
this.connection.off('unfollow', this.onChangeFollowing);
}
this.stream.dispose(this.connectionId);
document.removeEventListener('keydown', this.onKeydown);
}, },
methods: { methods: {
@ -96,13 +164,10 @@ export default Vue.extend({
this.fetching = true; this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => { (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api(this.endpoint, { (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1, limit: fetchLimit + 1,
untilDate: this.date ? this.date.getTime() : undefined, untilDate: this.date ? this.date.getTime() : undefined
includeMyRenotes: this.$store.state.settings.showMyRenotes, }, this.baseQuery, this.query)).then(notes => {
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
}).then(notes => {
if (notes.length == fetchLimit + 1) { if (notes.length == fetchLimit + 1) {
notes.pop(); notes.pop();
this.existMore = true; this.existMore = true;
@ -119,13 +184,10 @@ export default Vue.extend({
this.moreFetching = true; this.moreFetching = true;
const promise = (this as any).api(this.endpoint, { const promise = (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1, limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id, untilId: (this.$refs.timeline as any).tail().id
includeMyRenotes: this.$store.state.settings.showMyRenotes, }, this.baseQuery, this.query));
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
});
promise.then(notes => { promise.then(notes => {
if (notes.length == fetchLimit + 1) { if (notes.length == fetchLimit + 1) {
@ -140,15 +202,6 @@ export default Vue.extend({
return promise; return promise;
}, },
onNote(note) {
// Prepend a note
(this.$refs.timeline as any).prepend(note);
},
onChangeFollowing() {
this.fetch();
},
focus() { focus() {
(this.$refs.timeline as any).focus(); (this.$refs.timeline as any).focus();
}, },
@ -156,14 +209,6 @@ export default Vue.extend({
warp(date) { warp(date) {
this.date = date; this.date = date;
this.fetch(); this.fetch();
},
onKeydown(e) {
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
if (e.which == 84) { // t
this.focus();
}
}
} }
} }
}); });

View File

@ -5,13 +5,22 @@
<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span> <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span> <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl">%fa:hashtag% {{ tagTl.title }}</span>
<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span> <span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span>
<button @click="chooseList" title="%i18n:@list%">%fa:list%</button> <div class="buttons">
<button :data-active="src == 'mentions'" @click="src = 'mentions'" title="%i18n:@mentions%">%fa:at%<i class="badge" v-if="$store.state.i.hasUnreadMentions">%fa:circle%</i></button>
<button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%">%fa:envelope R%<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i></button>
<button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button>
<button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button>
</div>
</header> </header>
<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/> <x-core v-if="src == 'home'" ref="tl" key="home" src="home"/>
<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/> <x-core v-if="src == 'local'" ref="tl" key="local" src="local"/>
<x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> <x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/> <x-core v-if="src == 'global'" ref="tl" key="global" src="global"/>
<x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
<x-core v-if="src == 'messages'" ref="tl" key="messages" src="messages"/>
<x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/>
<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
</div> </div>
</template> </template>
@ -19,7 +28,8 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import XCore from './timeline.core.vue'; import XCore from './timeline.core.vue';
import MkUserListsWindow from './user-lists-window.vue'; import Menu from '../../../common/views/components/menu.vue';
import MkSettingsWindow from './settings-window.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -30,6 +40,7 @@ export default Vue.extend({
return { return {
src: 'home', src: 'home',
list: null, list: null,
tagTl: null,
enableLocalTimeline: false enableLocalTimeline: false
}; };
}, },
@ -39,8 +50,14 @@ export default Vue.extend({
this.saveSrc(); this.saveSrc();
}, },
list() { list(x) {
this.saveSrc(); this.saveSrc();
if (x != null) this.tagTl = null;
},
tagTl(x) {
this.saveSrc();
if (x != null) this.list = null;
} }
}, },
@ -53,6 +70,8 @@ export default Vue.extend({
this.src = this.$store.state.device.tl.src; this.src = this.$store.state.device.tl.src;
if (this.src == 'list') { if (this.src == 'list') {
this.list = this.$store.state.device.tl.arg; this.list = this.$store.state.device.tl.arg;
} else if (this.src == 'tag') {
this.tagTl = this.$store.state.device.tl.arg;
} }
} else if (this.$store.state.i.followingCount == 0) { } else if (this.$store.state.i.followingCount == 0) {
this.src = 'hybrid'; this.src = 'hybrid';
@ -69,20 +88,86 @@ export default Vue.extend({
saveSrc() { saveSrc() {
this.$store.commit('device/setTl', { this.$store.commit('device/setTl', {
src: this.src, src: this.src,
arg: this.list arg: this.src == 'list' ? this.list : this.tagTl
}); });
}, },
focus() {
(this.$refs.tl as any).focus();
},
warp(date) { warp(date) {
(this.$refs.tl as any).warp(date); (this.$refs.tl as any).warp(date);
}, },
chooseList() { async chooseList() {
const w = (this as any).os.new(MkUserListsWindow); const lists = await (this as any).api('users/lists/list');
w.$once('choosen', list => {
this.list = list; let menu = [{
this.src = 'list'; icon: '%fa:plus%',
w.close(); text: '%i18n:@add-list%',
action: () => {
(this as any).apis.input({
title: '%i18n:@list-name%',
}).then(async title => {
const list = await (this as any).api('users/lists/create', {
title
});
this.list = list;
this.src = 'list';
});
}
}];
if (lists.length > 0) {
menu.push(null);
}
menu = menu.concat(lists.map(list => ({
icon: '%fa:list%',
text: list.title,
action: () => {
this.list = list;
this.src = 'list';
}
})));
this.os.new(Menu, {
source: this.$refs.listButton,
compact: false,
items: menu
});
},
chooseTag() {
let menu = [{
icon: '%fa:plus%',
text: '%i18n:@add-tag-timeline%',
action: () => {
(this as any).os.new(MkSettingsWindow, {
initialPage: 'hashtags'
});
}
}];
if (this.$store.state.settings.tagTimelines.length > 0) {
menu.push(null);
}
menu = menu.concat(this.$store.state.settings.tagTimelines.map(t => ({
icon: '%fa:hashtag%',
text: t.title,
action: () => {
this.tagTl = t;
this.src = 'tag';
}
})));
this.os.new(Menu, {
source: this.$refs.tagButton,
compact: false,
items: menu
}); });
} }
} }
@ -104,22 +189,45 @@ root(isDark)
border-radius 6px 6px 0 0 border-radius 6px 6px 0 0
box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08) box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
> button > .buttons
position absolute position absolute
z-index 2 z-index 2
top 0 top 0
right 0 right 0
padding 0 padding-right 8px
width 42px
font-size 0.9em
line-height 42px
color isDark ? #9baec8 : #ccc
&:hover > button
color isDark ? #b2c1d5 : #aaa padding 0 8px
font-size 0.9em
line-height 42px
color isDark ? #9baec8 : #ccc
&:active > .badge
color isDark ? #b2c1d5 : #999 position absolute
top -4px
right 4px
font-size 10px
color $theme-color
&:hover
color isDark ? #b2c1d5 : #aaa
&:active
color isDark ? #b2c1d5 : #999
&[data-active]
color $theme-color
cursor default
&:before
content ""
display block
position absolute
bottom 0
left 0
width 100%
height 2px
background $theme-color
> span > span
display inline-block display inline-block

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="account"> <div class="account" v-hotkey.global="keymap">
<button class="header" :data-active="isOpen" @click="toggle"> <button class="header" :data-active="isOpen" @click="toggle">
<span class="username">{{ $store.state.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span> <span class="username">{{ $store.state.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
<mk-avatar class="avatar" :user="$store.state.i"/> <mk-avatar class="avatar" :user="$store.state.i"/>
@ -63,6 +63,13 @@ export default Vue.extend({
isOpen: false isOpen: false
}; };
}, },
computed: {
keymap(): any {
return {
'a|m': this.toggle
};
}
},
beforeDestroy() { beforeDestroy() {
this.close(); this.close();
}, },

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="notifications"> <div class="notifications" v-hotkey.global="keymap">
<button :data-active="isOpen" @click="toggle" title="%i18n:@title%"> <button :data-active="isOpen" @click="toggle" title="%i18n:@title%">
%fa:R bell%<template v-if="hasUnreadNotification">%fa:circle%</template> %fa:R bell%<template v-if="hasUnreadNotification">%fa:circle%</template>
</button> </button>
@ -19,11 +19,19 @@ export default Vue.extend({
isOpen: false isOpen: false
}; };
}, },
computed: { computed: {
hasUnreadNotification(): boolean { hasUnreadNotification(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
},
keymap(): any {
return {
'shift+n': this.toggle
};
} }
}, },
methods: { methods: {
toggle() { toggle() {
this.isOpen ? this.close() : this.open(); this.isOpen ? this.close() : this.open();

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mk-ui" :style="style"> <div class="mk-ui" :style="style" v-hotkey.global="keymap">
<x-header class="header" v-show="!zenMode"/> <x-header class="header" v-show="!zenMode"/>
<div class="content"> <div class="content">
<slot></slot> <slot></slot>
@ -16,11 +16,13 @@ export default Vue.extend({
components: { components: {
XHeader XHeader
}, },
data() { data() {
return { return {
zenMode: false zenMode: false
}; };
}, },
computed: { computed: {
style(): any { style(): any {
if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {}; if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {};
@ -28,27 +30,24 @@ export default Vue.extend({
backgroundColor: this.$store.state.i.wallpaperColor && this.$store.state.i.wallpaperColor.length == 3 ? `rgb(${ this.$store.state.i.wallpaperColor.join(',') })` : null, backgroundColor: this.$store.state.i.wallpaperColor && this.$store.state.i.wallpaperColor.length == 3 ? `rgb(${ this.$store.state.i.wallpaperColor.join(',') })` : null,
backgroundImage: `url(${ this.$store.state.i.wallpaperUrl })` backgroundImage: `url(${ this.$store.state.i.wallpaperUrl })`
}; };
},
keymap(): any {
return {
'p': this.post,
'n': this.post,
'z': this.toggleZenMode
};
} }
}, },
mounted() {
document.addEventListener('keydown', this.onKeydown);
},
beforeDestroy() {
document.removeEventListener('keydown', this.onKeydown);
},
methods: { methods: {
onKeydown(e) { post() {
if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return; (this as any).apis.post();
},
if (e.which == 80 || e.which == 78) { // p or n toggleZenMode() {
e.preventDefault(); this.zenMode = !this.zenMode;
(this as any).apis.post();
}
if (e.which == 90) { // z
e.preventDefault();
this.zenMode = !this.zenMode;
}
} }
} }
}); });

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy"> <mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
<span slot="header">%fa:list% %i18n:@title%</span> <span slot="header">%fa:list% %i18n:@title%</span>
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp" :data-darkmode="$store.state.device.darkmode"> <div class="xkxvokkjlptzyewouewmceqcxhpgzprp" :data-darkmode="$store.state.device.darkmode">

View File

@ -76,6 +76,11 @@ export default Vue.extend({
name: { name: {
type: String, type: String,
default: null default: null
},
animation: {
type: Boolean,
required: false,
default: true
} }
}, },
@ -142,7 +147,7 @@ export default Vue.extend({
anime({ anime({
targets: bg, targets: bg,
opacity: 1, opacity: 1,
duration: 100, duration: this.animation ? 100 : 0,
easing: 'linear' easing: 'linear'
}); });
} }
@ -152,7 +157,7 @@ export default Vue.extend({
targets: main, targets: main,
opacity: 1, opacity: 1,
scale: [1.1, 1], scale: [1.1, 1],
duration: 200, duration: this.animation ? 200 : 0,
easing: 'easeOutQuad' easing: 'easeOutQuad'
}); });
@ -160,7 +165,7 @@ export default Vue.extend({
setTimeout(() => { setTimeout(() => {
this.$emit('opened'); this.$emit('opened');
}, 300); }, this.animation ? 300 : 0);
}, },
close() { close() {
@ -174,7 +179,7 @@ export default Vue.extend({
anime({ anime({
targets: bg, targets: bg,
opacity: 0, opacity: 0,
duration: 300, duration: this.animation ? 300 : 0,
easing: 'linear' easing: 'linear'
}); });
} }
@ -185,14 +190,14 @@ export default Vue.extend({
targets: main, targets: main,
opacity: 0, opacity: 0,
scale: 0.8, scale: 0.8,
duration: 300, duration: this.animation ? 300 : 0,
easing: [0.5, -0.5, 1, 0.5] easing: [0.5, -0.5, 1, 0.5]
}); });
setTimeout(() => { setTimeout(() => {
this.destroyDom();
this.$emit('closed'); this.$emit('closed');
}, 300); this.destroyDom();
}, this.animation ? 300 : 0);
}, },
popout() { popout() {

View File

@ -14,6 +14,14 @@
</div> </div>
<div class="form"> <div class="form">
<div>
<label>
<p>%i18n:@banner-url%</p>
<input v-model="bannerUrl">
</label>
<button class="ui" @click="updateMeta">%i18n:@save%</button>
</div>
<div> <div>
<label> <label>
<input type="checkbox" v-model="disableRegistration" @change="updateMeta"> <input type="checkbox" v-model="disableRegistration" @change="updateMeta">
@ -46,6 +54,7 @@ export default Vue.extend({
stats: null, stats: null,
disableRegistration: false, disableRegistration: false,
disableLocalTimeline: false, disableLocalTimeline: false,
bannerUrl: null,
inviteCode: null, inviteCode: null,
connection: null, connection: null,
connectionId: null connectionId: null
@ -58,6 +67,7 @@ export default Vue.extend({
(this as any).os.getMeta().then(meta => { (this as any).os.getMeta().then(meta => {
this.disableRegistration = meta.disableRegistration; this.disableRegistration = meta.disableRegistration;
this.disableLocalTimeline = meta.disableLocalTimeline; this.disableLocalTimeline = meta.disableLocalTimeline;
this.bannerUrl = meta.bannerUrl;
}); });
(this as any).api('stats').then(stats => { (this as any).api('stats').then(stats => {
@ -76,7 +86,8 @@ export default Vue.extend({
updateMeta() { updateMeta() {
(this as any).api('admin/update-meta', { (this as any).api('admin/update-meta', {
disableRegistration: this.disableRegistration, disableRegistration: this.disableRegistration,
disableLocalTimeline: this.disableLocalTimeline disableLocalTimeline: this.disableLocalTimeline,
bannerUrl: this.bannerUrl
}); });
} }
} }
@ -114,6 +125,7 @@ export default Vue.extend({
> .form > .form
> div > div
padding 16px
border-bottom solid 1px #eee border-bottom solid 1px #eee
</style> </style>

View File

@ -6,6 +6,9 @@
<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked"/> <x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/> <x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/> <x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked"/>
<x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked"/>
<x-direct-column v-else-if="column.type == 'direct'" :column="column" :is-stacked="isStacked"/>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -13,12 +16,16 @@ import Vue from 'vue';
import XTlColumn from './deck.tl-column.vue'; import XTlColumn from './deck.tl-column.vue';
import XNotificationsColumn from './deck.notifications-column.vue'; import XNotificationsColumn from './deck.notifications-column.vue';
import XWidgetsColumn from './deck.widgets-column.vue'; import XWidgetsColumn from './deck.widgets-column.vue';
import XMentionsColumn from './deck.mentions-column.vue';
import XDirectColumn from './deck.direct-column.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XTlColumn, XTlColumn,
XNotificationsColumn, XNotificationsColumn,
XWidgetsColumn XWidgetsColumn,
XMentionsColumn,
XDirectColumn
}, },
props: { props: {

View File

@ -0,0 +1,38 @@
<template>
<x-column :name="name" :column="column" :is-stacked="isStacked">
<span slot="header">%fa:envelope R%{{ name }}</span>
<x-direct/>
</x-column>
</template>
<script lang="ts">
import Vue from 'vue';
import XColumn from './deck.column.vue';
import XDirect from './deck.direct.vue';
export default Vue.extend({
components: {
XColumn,
XDirect
},
props: {
column: {
type: Object,
required: true
},
isStacked: {
type: Boolean,
required: true
}
},
computed: {
name(): string {
if (this.column.name) return this.column.name;
return '%i18n:common.deck.direct%';
}
},
});
</script>

View File

@ -0,0 +1,97 @@
<template>
<x-notes ref="timeline" :more="existMore ? more : null"/>
</template>
<script lang="ts">
import Vue from 'vue';
import XNotes from './deck.notes.vue';
const fetchLimit = 10;
export default Vue.extend({
components: {
XNotes
},
props: {
},
data() {
return {
fetching: true,
moreFetching: false,
existMore: false,
connection: null,
connectionId: null
};
},
mounted() {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
this.connection.on('mention', this.onNote);
this.fetch();
},
beforeDestroy() {
this.connection.off('mention', this.onNote);
(this as any).os.stream.dispose(this.connectionId);
},
methods: {
fetch() {
this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api('notes/mentions', {
limit: fetchLimit + 1,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
visibility: 'specified'
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
}
res(notes);
this.fetching = false;
this.$emit('loaded');
}, rej);
}));
},
more() {
this.moreFetching = true;
const promise = (this as any).api('notes/mentions', {
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
visibility: 'specified'
});
promise.then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
} else {
this.existMore = false;
}
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
return promise;
},
onNote(note) {
// Prepend a note
if (note.visibility == 'specified') {
(this.$refs.timeline as any).prepend(note);
}
}
}
});
</script>

View File

@ -0,0 +1,117 @@
<template>
<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
</template>
<script lang="ts">
import Vue from 'vue';
import XNotes from './deck.notes.vue';
import { HashtagStream } from '../../../../common/scripts/streaming/hashtag';
const fetchLimit = 10;
export default Vue.extend({
components: {
XNotes
},
props: {
tagTl: {
type: Object,
required: true
},
mediaOnly: {
type: Boolean,
required: false,
default: false
},
mediaView: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
fetching: true,
moreFetching: false,
existMore: false,
connection: null
};
},
watch: {
mediaOnly() {
this.fetch();
}
},
mounted() {
if (this.connection) this.connection.close();
this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
this.connection.on('note', this.onNote);
this.fetch();
},
beforeDestroy() {
this.connection.close();
},
methods: {
fetch() {
this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api('notes/search_by_tag', {
limit: fetchLimit + 1,
withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
query: this.tagTl.query
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
}
res(notes);
this.fetching = false;
this.$emit('loaded');
}, rej);
}));
},
more() {
this.moreFetching = true;
const promise = (this as any).api('notes/search_by_tag', {
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id,
withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
query: this.tagTl.query
});
promise.then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
} else {
this.existMore = false;
}
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
return promise;
},
onNote(note) {
if (this.mediaOnly && note.files.length == 0) return;
// Prepend a note
(this.$refs.timeline as any).prepend(note);
}
}
});
</script>

View File

@ -0,0 +1,38 @@
<template>
<x-column :name="name" :column="column" :is-stacked="isStacked">
<span slot="header">%fa:at%{{ name }}</span>
<x-mentions/>
</x-column>
</template>
<script lang="ts">
import Vue from 'vue';
import XColumn from './deck.column.vue';
import XMentions from './deck.mentions.vue';
export default Vue.extend({
components: {
XColumn,
XMentions
},
props: {
column: {
type: Object,
required: true
},
isStacked: {
type: Boolean,
required: true
}
},
computed: {
name(): string {
if (this.column.name) return this.column.name;
return '%i18n:common.deck.mentions%';
}
},
});
</script>

View File

@ -0,0 +1,93 @@
<template>
<x-notes ref="timeline" :more="existMore ? more : null"/>
</template>
<script lang="ts">
import Vue from 'vue';
import XNotes from './deck.notes.vue';
const fetchLimit = 10;
export default Vue.extend({
components: {
XNotes
},
props: {
},
data() {
return {
fetching: true,
moreFetching: false,
existMore: false,
connection: null,
connectionId: null
};
},
mounted() {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
this.connection.on('mention', this.onNote);
this.fetch();
},
beforeDestroy() {
this.connection.off('mention', this.onNote);
(this as any).os.stream.dispose(this.connectionId);
},
methods: {
fetch() {
this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api('notes/mentions', {
limit: fetchLimit + 1,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
}
res(notes);
this.fetching = false;
this.$emit('loaded');
}, rej);
}));
},
more() {
this.moreFetching = true;
const promise = (this as any).api('notes/mentions', {
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
});
promise.then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
} else {
this.existMore = false;
}
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
return promise;
},
onNote(note) {
// Prepend a note
(this.$refs.timeline as any).prepend(note);
}
}
});
</script>

View File

@ -147,10 +147,14 @@ export default Vue.extend({
methods: { methods: {
capture(withHandler = false) { capture(withHandler = false) {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection.send({ const data = {
type: 'capture', type: 'capture',
id: this.p.id id: this.p.id
}); } as any;
if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
data.read = true;
}
this.connection.send(data);
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
} }
}, },

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="oxynyeqmfvracxnglgulyqfgqxnxmehl"> <div class="oxynyeqmfvracxnglgulyqfgqxnxmehl">
<!-- トランジションを有効にするとなぜかメモリリークする --> <!-- トランジションを有効にするとなぜかメモリリークする -->
<component :is="$store.state.device.animations ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications"> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications">
<template v-for="(notification, i) in _notifications"> <template v-for="(notification, i) in _notifications">
<x-notification class="notification" :notification="notification" :key="notification.id"/> <x-notification class="notification" :notification="notification" :key="notification.id"/>
<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">

View File

@ -6,6 +6,7 @@
<template v-if="column.type == 'hybrid'">%fa:share-alt%</template> <template v-if="column.type == 'hybrid'">%fa:share-alt%</template>
<template v-if="column.type == 'global'">%fa:globe%</template> <template v-if="column.type == 'global'">%fa:globe%</template>
<template v-if="column.type == 'list'">%fa:list%</template> <template v-if="column.type == 'list'">%fa:list%</template>
<template v-if="column.type == 'hashtag'">%fa:hashtag%</template>
<span>{{ name }}</span> <span>{{ name }}</span>
</span> </span>
@ -14,6 +15,7 @@
<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/> <mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/>
</div> </div>
<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/> <x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
<x-hashtag-tl v-if="column.type == 'hashtag'" :tag-tl="$store.state.settings.tagTimelines.find(x => x.id == column.tagTlId)" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/> <x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
</x-column> </x-column>
</template> </template>
@ -23,12 +25,14 @@ import Vue from 'vue';
import XColumn from './deck.column.vue'; import XColumn from './deck.column.vue';
import XTl from './deck.tl.vue'; import XTl from './deck.tl.vue';
import XListTl from './deck.list-tl.vue'; import XListTl from './deck.list-tl.vue';
import XHashtagTl from './deck.hashtag-tl.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XColumn, XColumn,
XTl, XTl,
XListTl XListTl,
XHashtagTl
}, },
props: { props: {
@ -65,6 +69,7 @@ export default Vue.extend({
case 'hybrid': return '%i18n:common.deck.hybrid%'; case 'hybrid': return '%i18n:common.deck.hybrid%';
case 'global': return '%i18n:common.deck.global%'; case 'global': return '%i18n:common.deck.global%';
case 'list': return this.column.list.title; case 'list': return this.column.list.title;
case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title;
} }
} }
}, },

View File

@ -138,6 +138,24 @@ export default Vue.extend({
type: 'global' type: 'global'
}); });
} }
}, {
icon: '%fa:at%',
text: '%i18n:common.deck.mentions%',
action: () => {
this.$store.dispatch('settings/addDeckColumn', {
id: uuid(),
type: 'mentions'
});
}
}, {
icon: '%fa:envelope R%',
text: '%i18n:common.deck.direct%',
action: () => {
this.$store.dispatch('settings/addDeckColumn', {
id: uuid(),
type: 'direct'
});
}
}, { }, {
icon: '%fa:list%', icon: '%fa:list%',
text: '%i18n:common.deck.list%', text: '%i18n:common.deck.list%',
@ -152,6 +170,20 @@ export default Vue.extend({
w.close(); w.close();
}); });
} }
}, {
icon: '%fa:hashtag%',
text: '%i18n:common.deck.hashtag%',
action: () => {
(this as any).apis.input({
title: '%i18n:@enter-hashtag-tl-title%'
}).then(title => {
this.$store.dispatch('settings/addDeckColumn', {
id: uuid(),
type: 'hashtag',
tagTlId: this.$store.state.settings.tagTimelines.find(x => x.title == title).id
});
});
}
}, { }, {
icon: '%fa:bell R%', icon: '%fa:bell R%',
text: '%i18n:common.deck.notifications%', text: '%i18n:common.deck.notifications%',

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<mk-home :mode="mode" @loaded="loaded"/> <mk-home :mode="mode" @loaded="loaded" ref="home" v-hotkey.global="keymap"/>
</mk-ui> </mk-ui>
</template> </template>
@ -15,6 +15,13 @@ export default Vue.extend({
default: 'timeline' default: 'timeline'
} }
}, },
computed: {
keymap(): any {
return {
't': this.focus
};
}
},
mounted() { mounted() {
document.title = (this as any).os.instanceName; document.title = (this as any).os.instanceName;
@ -23,6 +30,9 @@ export default Vue.extend({
methods: { methods: {
loaded() { loaded() {
Progress.done(); Progress.done();
},
focus() {
this.$refs.home.focus();
} }
} }
}); });

View File

@ -43,7 +43,7 @@ export default Vue.extend({
> .stats > .stats
display flex display flex
justify-content center justify-content center
margin-bottom 16px margin 0 auto 16px auto
padding 32px padding 32px
background #fff background #fff
box-shadow 0 2px 8px rgba(#000, 0.1) box-shadow 0 2px 8px rgba(#000, 0.1)
@ -60,5 +60,6 @@ export default Vue.extend({
font-size 70% font-size 70%
> div > div
max-width 850px max-width 950px
margin 0 auto
</style> </style>

View File

@ -6,7 +6,7 @@
<main> <main>
<div class="main"> <div class="main">
<x-header :user="user"/> <x-header :user="user"/>
<mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/> <mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
<x-timeline class="timeline" ref="tl" :user="user"/> <x-timeline class="timeline" ref="tl" :user="user"/>
</div> </div>
<div class="side"> <div class="side">
@ -28,7 +28,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import parseAcct from '../../../../../../misc/acct/parse'; import parseAcct from '../../../../../../misc/acct/parse';
import getUserName from '../../../../../../misc/get-user-name';
import Progress from '../../../../common/scripts/loading'; import Progress from '../../../../common/scripts/loading';
import XHeader from './user.header.vue'; import XHeader from './user.header.vue';
import XTimeline from './user.timeline.vue'; import XTimeline from './user.timeline.vue';

View File

@ -1,5 +1,7 @@
<template> <template>
<div class="mk-welcome"> <div class="mk-welcome">
<div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div>
<button @click="dark"> <button @click="dark">
<template v-if="$store.state.device.darkmode">%fa:moon%</template> <template v-if="$store.state.device.darkmode">%fa:moon%</template>
<template v-else>%fa:R moon%</template> <template v-else>%fa:R moon%</template>
@ -154,6 +156,7 @@ export default Vue.extend({
return { return {
meta: null, meta: null,
stats: null, stats: null,
banner: null,
copyright, copyright,
host, host,
name: 'Misskey', name: 'Misskey',
@ -169,6 +172,7 @@ export default Vue.extend({
this.name = meta.name; this.name = meta.name;
this.description = meta.description; this.description = meta.description;
this.announcements = meta.broadcasts; this.announcements = meta.broadcasts;
this.banner = meta.bannerUrl;
}); });
(this as any).api('stats').then(stats => { (this as any).api('stats').then(stats => {
@ -308,6 +312,26 @@ root(isDark)
//background-position center //background-position center
//background-size cover //background-size cover
> .banner
position absolute
top 0
left 0
width 100%
height 400px
background-position center
background-size cover
opacity 0.7
&:after
content ""
display block
position absolute
bottom 0
left 0
width 100%
height 100px
background linear-gradient(transparent, isDark ? #191b22 : #f7f7f7)
> .forkit > .forkit
position absolute position absolute
top 0 top 0

View File

@ -8,6 +8,7 @@ import VueRouter from 'vue-router';
import * as TreeView from 'vue-json-tree-view'; import * as TreeView from 'vue-json-tree-view';
import VAnimateCss from 'v-animate-css'; import VAnimateCss from 'v-animate-css';
import VModal from 'vue-js-modal'; import VModal from 'vue-js-modal';
import VueHotkey from './common/hotkey';
import App from './app.vue'; import App from './app.vue';
import checkForUpdate from './common/scripts/check-for-update'; import checkForUpdate from './common/scripts/check-for-update';
@ -19,6 +20,7 @@ Vue.use(VueRouter);
Vue.use(TreeView); Vue.use(TreeView);
Vue.use(VAnimateCss); Vue.use(VAnimateCss);
Vue.use(VModal); Vue.use(VModal);
Vue.use(VueHotkey);
// Register global directives // Register global directives
require('./common/views/directives'); require('./common/views/directives');

View File

@ -160,10 +160,14 @@ export default Vue.extend({
methods: { methods: {
capture(withHandler = false) { capture(withHandler = false) {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection.send({ const data = {
type: 'capture', type: 'capture',
id: this.p.id id: this.p.id
}); } as any;
if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
data.read = true;
}
this.connection.send(data);
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
} }
}, },

View File

@ -14,7 +14,7 @@
</div> </div>
<!-- トランジションを有効にするとなぜかメモリリークする --> <!-- トランジションを有効にするとなぜかメモリリークする -->
<component :is="$store.state.device.animations ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div"> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div">
<template v-for="(note, i) in _notes"> <template v-for="(note, i) in _notes">
<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> <mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="mk-notifications"> <div class="mk-notifications">
<!-- トランジションを有効にするとなぜかメモリリークする --> <!-- トランジションを有効にするとなぜかメモリリークする -->
<component :is="$store.state.device.animations ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications"> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications">
<template v-for="(notification, i) in _notifications"> <template v-for="(notification, i) in _notifications">
<mk-notification :notification="notification" :key="notification.id"/> <mk-notification :notification="notification" :key="notification.id"/>
<p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date"> <p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date">

View File

@ -188,9 +188,6 @@ root(isDark)
overflow hidden overflow hidden
text-overflow ellipsis text-overflow ellipsis
[data-fa], [data-icon]
margin-right 4px
> img > img
display inline-block display inline-block
vertical-align bottom vertical-align bottom

View File

@ -1,9 +1,9 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header"> <span slot="header">
<template v-if="folder">%fa:R folder-open%{{ folder.name }}</template> <template v-if="folder"><span style="margin-right:4px;">%fa:R folder-open%</span>{{ folder.name }}</template>
<template v-if="file"><mk-file-type-icon data-icon :type="file.type"/>{{ file.name }}</template> <template v-if="file"><mk-file-type-icon data-icon :type="file.type" style="margin-right:4px;"/>{{ file.name }}</template>
<template v-if="!folder && !file">%fa:cloud%%i18n:@drive%</template> <template v-if="!folder && !file"><span style="margin-right:4px;">%fa:cloud%</span>%i18n:@drive%</template>
</span> </span>
<template slot="func"><button @click="fn">%fa:ellipsis-h%</button></template> <template slot="func"><button @click="fn">%fa:ellipsis-h%</button></template>
<mk-drive <mk-drive

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:star%%i18n:@title%</span> <span slot="header"><span style="margin-right:4px;">%fa:star%</span>%i18n:@title%</span>
<main> <main>
<template v-for="favorite in favorites"> <template v-for="favorite in favorites">

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:gamepad%%i18n:@reversi%</span> <span slot="header"><span style="margin-right:4px;">%fa:gamepad%</span>%i18n:@reversi%</span>
<mk-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/> <mk-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/>
</mk-ui> </mk-ui>
</template> </template>

View File

@ -13,6 +13,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
const fetchLimit = 10; const fetchLimit = 10;
@ -21,6 +22,9 @@ export default Vue.extend({
src: { src: {
type: String, type: String,
required: true required: true
},
tagTl: {
required: false
} }
}, },
@ -29,10 +33,18 @@ export default Vue.extend({
fetching: true, fetching: true,
moreFetching: false, moreFetching: false,
existMore: false, existMore: false,
streamManager: null,
connection: null, connection: null,
connectionId: null, connectionId: null,
unreadCount: 0, unreadCount: 0,
date: null date: null,
baseQuery: {
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
},
query: {},
endpoint: null
}; };
}, },
@ -41,49 +53,109 @@ export default Vue.extend({
return this.$store.state.i.followingCount == 0; return this.$store.state.i.followingCount == 0;
}, },
stream(): any {
switch (this.src) {
case 'home': return (this as any).os.stream;
case 'local': return (this as any).os.streams.localTimelineStream;
case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
case 'global': return (this as any).os.streams.globalTimelineStream;
}
},
endpoint(): string {
switch (this.src) {
case 'home': return 'notes/timeline';
case 'local': return 'notes/local-timeline';
case 'hybrid': return 'notes/hybrid-timeline';
case 'global': return 'notes/global-timeline';
}
},
canFetchMore(): boolean { canFetchMore(): boolean {
return !this.moreFetching && !this.fetching && this.existMore; return !this.moreFetching && !this.fetching && this.existMore;
} }
}, },
mounted() { mounted() {
this.connection = this.stream.getConnection(); const prepend = note => {
this.connectionId = this.stream.use(); (this.$refs.timeline as any).prepend(note);
};
this.connection.on('note', this.onNote); if (this.src == 'tag') {
if (this.src == 'home') { this.endpoint = 'notes/search_by_tag';
this.connection.on('follow', this.onChangeFollowing); this.query = {
this.connection.on('unfollow', this.onChangeFollowing); query: this.tagTl.query
};
this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.connection.close();
});
} else if (this.src == 'home') {
this.endpoint = 'notes/timeline';
const onChangeFollowing = () => {
this.fetch();
};
this.streamManager = (this as any).os.stream;
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend);
this.connection.on('follow', onChangeFollowing);
this.connection.on('unfollow', onChangeFollowing);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.connection.off('follow', onChangeFollowing);
this.connection.off('unfollow', onChangeFollowing);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'local') {
this.endpoint = 'notes/local-timeline';
this.streamManager = (this as any).os.streams.localTimelineStream;
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'hybrid') {
this.endpoint = 'notes/hybrid-timeline';
this.streamManager = (this as any).os.streams.hybridTimelineStream;
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'global') {
this.endpoint = 'notes/global-timeline';
this.streamManager = (this as any).os.streams.globalTimelineStream;
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'mentions') {
this.endpoint = 'notes/mentions';
this.streamManager = (this as any).os.stream;
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('mention', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('mention', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'messages') {
this.endpoint = 'notes/mentions';
this.query = {
visibility: 'specified'
};
const onNote = note => {
if (note.visibility == 'specified') {
prepend(note);
}
};
this.streamManager = (this as any).os.stream;
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('mention', onNote);
this.$once('beforeDestroy', () => {
this.connection.off('mention', onNote);
this.streamManager.dispose(this.connectionId);
});
} }
this.fetch(); this.fetch();
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('note', this.onNote); this.$emit('beforeDestroy');
if (this.src == 'home') {
this.connection.off('follow', this.onChangeFollowing);
this.connection.off('unfollow', this.onChangeFollowing);
}
this.stream.dispose(this.connectionId);
}, },
methods: { methods: {
@ -91,13 +163,10 @@ export default Vue.extend({
this.fetching = true; this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => { (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api(this.endpoint, { (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1, limit: fetchLimit + 1,
untilDate: this.date ? this.date.getTime() : undefined, untilDate: this.date ? this.date.getTime() : undefined
includeMyRenotes: this.$store.state.settings.showMyRenotes, }, this.baseQuery, this.query)).then(notes => {
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
}).then(notes => {
if (notes.length == fetchLimit + 1) { if (notes.length == fetchLimit + 1) {
notes.pop(); notes.pop();
this.existMore = true; this.existMore = true;
@ -114,13 +183,10 @@ export default Vue.extend({
this.moreFetching = true; this.moreFetching = true;
const promise = (this as any).api(this.endpoint, { const promise = (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1, limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id, untilId: (this.$refs.timeline as any).tail().id
includeMyRenotes: this.$store.state.settings.showMyRenotes, }, this.baseQuery, this.query));
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
});
promise.then(notes => { promise.then(notes => {
if (notes.length == fetchLimit + 1) { if (notes.length == fetchLimit + 1) {
@ -135,15 +201,6 @@ export default Vue.extend({
return promise; return promise;
}, },
onNote(note) {
// Prepend a note
(this.$refs.timeline as any).prepend(note);
},
onChangeFollowing() {
this.fetch();
},
focus() { focus() {
(this.$refs.timeline as any).focus(); (this.$refs.timeline as any).focus();
}, },

View File

@ -1,17 +1,21 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header" @click="showNav = true"> <span slot="header" @click="showNav = true">
<span> <span :class="$style.title">
<span v-if="src == 'home'">%fa:home%%i18n:@home%</span> <span v-if="src == 'home'">%fa:home%%i18n:@home%</span>
<span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span> <span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span>
<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span> <span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span>
<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span> <span v-if="src == 'global'">%fa:globe%%i18n:@global%</span>
<span v-if="src == 'mentions'">%fa:at%%i18n:@mentions%</span>
<span v-if="src == 'messages'">%fa:envelope R%%i18n:@messages%</span>
<span v-if="src == 'list'">%fa:list%{{ list.title }}</span> <span v-if="src == 'list'">%fa:list%{{ list.title }}</span>
<span v-if="src == 'tag'">%fa:hashtag%{{ tagTl.title }}</span>
</span> </span>
<span style="margin-left:8px"> <span style="margin-left:8px">
<template v-if="!showNav">%fa:angle-down%</template> <template v-if="!showNav">%fa:angle-down%</template>
<template v-else>%fa:angle-up%</template> <template v-else>%fa:angle-up%</template>
</span> </span>
<i :class="$style.badge" v-if="$store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i>
</span> </span>
<template slot="func"> <template slot="func">
@ -21,15 +25,22 @@
<main :data-darkmode="$store.state.device.darkmode"> <main :data-darkmode="$store.state.device.darkmode">
<div class="nav" v-if="showNav"> <div class="nav" v-if="showNav">
<div class="bg" @click="showNav = false"></div> <div class="bg" @click="showNav = false"></div>
<div class="pointer"></div>
<div class="body"> <div class="body">
<div> <div>
<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span>
<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span> <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span> <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
<div class="hr"></div>
<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%<i class="badge" v-if="$store.state.i.hasUnreadMentions">%fa:circle%</i></span>
<span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i></span>
<template v-if="lists"> <template v-if="lists">
<div class="hr" v-if="lists.length > 0"></div>
<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span> <span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
</template> </template>
<div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div>
<span v-for="tl in $store.state.settings.tagTimelines" :data-active="src == 'tag' && tagTl == tl" @click="src = 'tag'; tagTl = tl" :key="tl.id">%fa:hashtag% {{ tl.title }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -39,6 +50,9 @@
<x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/> <x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/>
<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> <x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> <x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
<x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
<x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/>
<x-tl v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/>
<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
</div> </div>
</main> </main>
@ -60,6 +74,7 @@ export default Vue.extend({
src: 'home', src: 'home',
list: null, list: null,
lists: null, lists: null,
tagTl: null,
showNav: false, showNav: false,
enableLocalTimeline: false enableLocalTimeline: false
}; };
@ -71,9 +86,16 @@ export default Vue.extend({
this.saveSrc(); this.saveSrc();
}, },
list() { list(x) {
this.showNav = false; this.showNav = false;
this.saveSrc(); this.saveSrc();
if (x != null) this.tagTl = null;
},
tagTl(x) {
this.showNav = false;
this.saveSrc();
if (x != null) this.list = null;
}, },
showNav(v) { showNav(v) {
@ -94,6 +116,8 @@ export default Vue.extend({
this.src = this.$store.state.device.tl.src; this.src = this.$store.state.device.tl.src;
if (this.src == 'list') { if (this.src == 'list') {
this.list = this.$store.state.device.tl.arg; this.list = this.$store.state.device.tl.arg;
} else if (this.src == 'tag') {
this.tagTl = this.$store.state.device.tl.arg;
} }
} else if (this.$store.state.i.followingCount == 0) { } else if (this.$store.state.i.followingCount == 0) {
this.src = 'hybrid'; this.src = 'hybrid';
@ -118,7 +142,7 @@ export default Vue.extend({
saveSrc() { saveSrc() {
this.$store.commit('device/setTl', { this.$store.commit('device/setTl', {
src: this.src, src: this.src,
arg: this.list arg: this.src == 'list' ? this.list : this.tagTl
}); });
}, },
@ -134,6 +158,26 @@ export default Vue.extend({
root(isDark) root(isDark)
> .nav > .nav
> .pointer
position fixed
z-index 10002
top 56px
left 0
right 0
$size = 16px
&:after
content ""
display block
position absolute
top -($size * 2)
left s('calc(50% - %s)', $size)
border-top solid $size transparent
border-left solid $size transparent
border-right solid $size transparent
border-bottom solid $size isDark ? #272f3a : #fff
> .bg > .bg
position fixed position fixed
z-index 10000 z-index 10000
@ -150,28 +194,22 @@ root(isDark)
left 0 left 0
right 0 right 0
width 300px width 300px
max-height calc(100% - 70px)
margin 0 auto margin 0 auto
overflow auto
-webkit-overflow-scrolling touch
background isDark ? #272f3a : #fff background isDark ? #272f3a : #fff
border-radius 8px border-radius 8px
box-shadow 0 0 16px rgba(#000, 0.1) box-shadow 0 0 16px rgba(#000, 0.1)
$balloon-size = 16px
&:after
content ""
display block
position absolute
top -($balloon-size * 2) + 1.5px
left s('calc(50% - %s)', $balloon-size)
border-top solid $balloon-size transparent
border-left solid $balloon-size transparent
border-right solid $balloon-size transparent
border-bottom solid $balloon-size isDark ? #272f3a : #fff
> div > div
padding 8px 0 padding 8px 0
> * > .hr
margin 8px 0
border-top solid 1px isDark ? rgba(#000, 0.3) : rgba(#000, 0.1)
> *:not(.hr)
display block display block
padding 8px 16px padding 8px 16px
color isDark ? #cdd0d8 : #666 color isDark ? #cdd0d8 : #666
@ -183,6 +221,11 @@ root(isDark)
&:not([data-active]):hover &:not([data-active]):hover
background isDark ? #353e4a : #eee background isDark ? #353e4a : #eee
> .badge
margin-left 6px
font-size 10px
color $theme-color
> .tl > .tl
max-width 680px max-width 680px
margin 0 auto margin 0 auto
@ -201,3 +244,18 @@ main:not([data-darkmode])
root(false) root(false)
</style> </style>
<style lang="stylus" module>
@import '~const.styl'
.title
i
margin-right 4px
.badge
margin-left 6px
font-size 10px
color $theme-color
vertical-align middle
</style>

View File

@ -1,7 +1,7 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header"> <span slot="header">
<template v-if="user">%fa:R comments%{{ user | userName }}</template> <template v-if="user"><span style="margin-right:4px;">%fa:R comments%</span>{{ user | userName }}</template>
<template v-else><mk-ellipsis/></template> <template v-else><mk-ellipsis/></template>
</span> </span>
<mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/> <mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/>

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:R comments%%i18n:@messaging%</span> <span slot="header"><span style="margin-right:4px;">%fa:R comments%</span>%i18n:@messaging%</span>
<mk-messaging @navigate="navigate" :header-top="48"/> <mk-messaging @navigate="navigate" :header-top="48"/>
</mk-ui> </mk-ui>
</template> </template>

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:R sticky-note%%i18n:@title%</span> <span slot="header"><span style="margin-right:4px;">%fa:R sticky-note%</span>%i18n:@title%</span>
<main v-if="!fetching"> <main v-if="!fetching">
<div> <div>
<mk-note-detail :note="note"/> <mk-note-detail :note="note"/>

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:R bell%%i18n:@notifications%</span> <span slot="header"><span style="margin-right:4px;">%fa:R bell%</span>%i18n:@notifications%</span>
<template slot="func"><button @click="fn">%fa:check%</button></template> <template slot="func"><button @click="fn">%fa:check%</button></template>
<main> <main>

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:cog%%i18n:@settings%</span> <span slot="header"><span style="margin-right:4px;">%fa:cog%</span>%i18n:@settings%</span>
<main :data-darkmode="$store.state.device.darkmode"> <main :data-darkmode="$store.state.device.darkmode">
<div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${name}</b>`)"></div> <div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${name}</b>`)"></div>
@ -13,7 +13,7 @@
<section> <section>
<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch> <ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
<ui-switch v-model="circleIcons">%i18n:@circle-icons%</ui-switch> <ui-switch v-model="circleIcons">%i18n:@circle-icons%</ui-switch>
<ui-switch v-model="animations">%i18n:common.enable-animations% (%i18n:common.this-setting-is-this-device-only%)</ui-switch> <ui-switch v-model="reduceMotion">%i18n:common.reduce-motion% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
<ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch> <ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch>
<ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch> <ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch>
<ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch> <ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch>
@ -169,9 +169,9 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); } set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
}, },
animations: { reduceMotion: {
get() { return this.$store.state.device.animations; }, get() { return this.$store.state.device.reduceMotion; },
set(value) { this.$store.commit('device/set', { key: 'animations', value }); } set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
}, },
alwaysShowNsfw: { alwaysShowNsfw: {

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:hashtag%{{ $route.params.tag }}</span> <span slot="header"><span style="margin-right:4px;">%fa:hashtag%</span>{{ $route.params.tag }}</span>
<main> <main>
<p v-if="!fetching && empty">%fa:search% {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p> <p v-if="!fetching && empty">%fa:search% {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="root home"> <div class="root home">
<mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/> <mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
<section class="recent-notes"> <section class="recent-notes">
<h2>%fa:R comments%%i18n:@recent-notes%</h2> <h2>%fa:R comments%%i18n:@recent-notes%</h2>
<div> <div>

View File

@ -1,5 +1,7 @@
<template> <template>
<div class="wgwfgvvimdjvhjfwxropcwksnzftjqes"> <div class="wgwfgvvimdjvhjfwxropcwksnzftjqes">
<div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div>
<div> <div>
<img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"> <img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name">
<p class="host">{{ host }}</p> <p class="host">{{ host }}</p>
@ -80,6 +82,7 @@ export default Vue.extend({
meta: null, meta: null,
copyright, copyright,
stats: null, stats: null,
banner: null,
host, host,
name: 'Misskey', name: 'Misskey',
description: '', description: '',
@ -93,6 +96,7 @@ export default Vue.extend({
this.name = meta.name; this.name = meta.name;
this.description = meta.description; this.description = meta.description;
this.announcements = meta.broadcasts; this.announcements = meta.broadcasts;
this.banner = meta.bannerUrl;
}); });
(this as any).api('stats').then(stats => { (this as any).api('stats').then(stats => {
@ -121,7 +125,27 @@ root(isDark)
text-align center text-align center
//background #fff //background #fff
> div > .banner
position absolute
top 0
left 0
width 100%
height 300px
background-position center
background-size cover
opacity 0.7
&:after
content ""
display block
position absolute
bottom 0
left 0
width 100%
height 100px
background linear-gradient(transparent, isDark ? #191b22 : #f7f7f7)
> div:not(.banner)
padding 32px padding 32px
margin 0 auto margin 0 auto
max-width 500px max-width 500px

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:home%%i18n:@dashboard%</span> <span slot="header"><span style="margin-right:4px;">%fa:home%</span>%i18n:@dashboard%</span>
<template slot="func"> <template slot="func">
<button @click="customizing = !customizing">%fa:cog%</button> <button @click="customizing = !customizing">%fa:cog%</button>
</template> </template>

View File

@ -10,6 +10,7 @@ const defaultSettings = {
home: null, home: null,
mobileHome: [], mobileHome: [],
deck: null, deck: null,
tagTimelines: [],
fetchOnScroll: true, fetchOnScroll: true,
showMaps: true, showMaps: true,
showPostFormOnTopOfTl: false, showPostFormOnTopOfTl: false,
@ -38,7 +39,7 @@ const defaultSettings = {
}; };
const defaultDeviceSettings = { const defaultDeviceSettings = {
animations: true, reduceMotion: false,
apiViaStream: true, apiViaStream: true,
autoPopout: false, autoPopout: false,
darkmode: false, darkmode: false,

View File

@ -101,15 +101,15 @@ props:
ja-JP: "投稿の数" ja-JP: "投稿の数"
en-US: "The number of the notes of this user" en-US: "The number of the notes of this user"
pinnedNote: pinnedNotes:
type: "entity(Note)" type: "entity(Note)[]"
optional: true optional: true
desc: desc:
ja-JP: "ピン留めされた投稿" ja-JP: "ピン留めされた投稿"
en-US: "The pinned note of this user" en-US: "The pinned note of this user"
pinnedNoteId: pinnedNoteIds:
type: "id(Note)" type: "id(Note)[]"
optional: true optional: true
desc: desc:
ja-JP: "ピン留めされた投稿のID" ja-JP: "ピン留めされた投稿のID"

View File

@ -9,7 +9,7 @@ html(lang= lang)
link(rel="stylesheet" href="/docs/assets/style.css") link(rel="stylesheet" href="/docs/assets/style.css")
link(rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css") link(rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css")
script(src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js") script(src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js")
link(rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous") link(rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous")
block meta block meta
body body

View File

@ -0,0 +1,96 @@
# Misskeyキーボードショートカットまとめ
## グローバル
これらのショートカットは基本的にどこでも使えます。
<table>
<thead>
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>新規投稿</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
<tr><td><kbd class="key">T</kbd></td><td>タイムラインの最も新しい投稿にフォーカス</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
<tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>通知を表示/隠す</td><td><b>N</b>otifications</td></tr>
<tr><td><kbd class="key">A</kbd>, <kbd class="key">M</kbd></td><td>アカウントメニューを表示/隠す</td><td><b>A</b>ccount, <b>M</b>y, <b>M</b>e, <b>M</b>enu</td></tr>
<tr><td><kbd class="key">Z</kbd></td><td>上部のバーを隠す</td><td><b>Z</b>en</td></tr>
<tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>ヘルプを表示</td><td><b>H</b>elp</td></tr>
</tbody>
</table>
## 投稿にフォーカスされた状態
<table>
<thead>
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key"></kbd>, <kbd class="key">K</kbd>, <kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">Tab</kbd></kbd></td><td>上の投稿にフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key"></kbd>, <kbd class="key">J</kbd>, <kbd class="key">Tab</kbd></td><td>下の投稿にフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key"></kbd>, <kbd class="key">R</kbd></td><td>返信フォームを開く</td><td><b>R</b>eply</td></tr>
<tr><td><kbd class="key"></kbd>, <kbd class="key">Q</kbd></td><td>Renoteフォームを開く</td><td><b>Q</b>uote</td></tr>
<tr><td><kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key"></kbd></kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">Q</kbd></kbd></td><td>即刻Renoteする(フォームを開かずに)</td><td>-</td></tr>
<tr><td><kbd class="key">E</kbd>, <kbd class="key">A</kbd>, <kbd class="key">+</kbd></td><td>リアクションフォームを開く</td><td><b>E</b>mote, re<b>A</b>ction</td></tr>
<tr><td><kbd class="key">0</kbd>~<kbd class="key">9</kbd></td><td>数字に対応したリアクションをする(対応については後述)</td><td>-</td></tr>
<tr><td><kbd class="key">M</kbd>, <kbd class="key">O</kbd></td><td>投稿に対するメニューを開く</td><td><b>M</b>ore, <b>O</b>ther</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>CWで隠された部分を表示 or 隠す</td><td><b>S</b>how, <b>S</b>ee</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>フォーカスを外す</td><td>-</td></tr>
</tbody>
</table>
## Renoteフォーム
<table>
<thead>
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">Enter</kbd></td><td>Renoteする</td><td>-</td></tr>
<tr><td><kbd class="key">Q</kbd></td><td>フォームを展開する</td><td><b>Q</b>uote</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>フォームを閉じる</td><td>-</td></tr>
</tbody>
</table>
## リアクションフォーム
デフォルトで「👍」にフォーカスが当たっている状態です。
<table>
<thead>
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key"></kbd>, <kbd class="key">K</kbd></td><td>上のリアクションにフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key"></kbd>, <kbd class="key">J</kbd></td><td>下のリアクションにフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key"></kbd>, <kbd class="key">H</kbd>, <kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">Tab</kbd></kbd></td><td>左のリアクションにフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key"></kbd>, <kbd class="key">L</kbd>, <kbd class="key">Tab</kbd></td><td>右のリアクションにフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key">Enter</kbd>, <kbd class="key">Space</kbd>, <kbd class="key">+</kbd></td><td>リアクション確定</td><td>-</td></tr>
<tr><td><kbd class="key">0</kbd>~<kbd class="key">9</kbd></td><td>数字に対応したリアクションで確定(対応については後述)</td><td>-</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>リアクションするのをやめる</td><td>-</td></tr>
</tbody>
</table>
## リアクションと数字キーの対応
<table>
<thead>
<tr><th>数字キー</th><th>リアクション</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">1</kbd></td><td>👍</td></tr>
<tr><td><kbd class="key">2</kbd></td><td>❤️</td></tr>
<tr><td><kbd class="key">3</kbd></td><td>😆</td></tr>
<tr><td><kbd class="key">4</kbd></td><td>🤔</td></tr>
<tr><td><kbd class="key">5</kbd></td><td>😮</td></tr>
<tr><td><kbd class="key">6</kbd></td><td>🎉</td></tr>
<tr><td><kbd class="key">7</kbd></td><td>💢</td></tr>
<tr><td><kbd class="key">8</kbd></td><td>😥</td></tr>
<tr><td><kbd class="key">9</kbd></td><td>😇</td></tr>
<tr><td><kbd class="key">0</kbd></td><td>🍮 or 🍣</td></tr>
</tbody>
</table>
# 例
<table>
<thead>
<tr><th>ショートカット</th><th>動作</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">t</kbd><kbd class="key">+</kbd><kbd class="key">+</kbd></td><td>タイムラインの最新の投稿に👍する</td></tr>
<tr><td><kbd class="key">t</kbd><kbd class="key">1</kbd></td><td>タイムラインの最新の投稿に👍する</td></tr>
<tr><td><kbd class="key">t</kbd><kbd class="key">0</kbd></td><td>タイムラインの最新の投稿に🍮する</td></tr>
</tbody>
</table>

View File

@ -128,3 +128,24 @@ pre
> code > code
display block display block
padding 16px padding 16px
kbd.group
display inline-block
padding 4px
background #fbfbfb
border 1px solid #d6d6d6
border-radius 4px
box-shadow 0 1px 1px rgba(0, 0, 0, 0.1)
kbd.key
display inline-block
padding 6px 8px
background #fff
border solid 1px #cecece
border-radius 4px
box-shadow 0 1px 1px rgba(0, 0, 0, 0.1)
td
> kbd.group,
> kbd.key
margin 4px

View File

@ -82,8 +82,12 @@ const handlers: { [key: string]: (window: any, token: any, mentionedRemoteUsers:
text({ document }, { content }) { text({ document }, { content }) {
const nodes = (content as string).split('\n').map(x => document.createTextNode(x)); const nodes = (content as string).split('\n').map(x => document.createTextNode(x));
for (const x of intersperse(document.createElement('br'), nodes)) { for (const x of intersperse('br', nodes)) {
document.body.appendChild(x); if (x === 'br') {
document.body.appendChild(document.createElement('br'));
} else {
document.body.appendChild(x);
}
} }
}, },

View File

@ -9,9 +9,9 @@ export type TextElementHashtag = {
}; };
export default function(text: string, i: number) { export default function(text: string, i: number) {
if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null; if (!(/^\s#[^\s\.,]+/.test(text) || (i == 0 && /^#[^\s\.,]+/.test(text)))) return null;
const isHead = text.startsWith('#'); const isHead = text.startsWith('#');
const hashtag = text.match(/^\s?#[^\s]+/)[0]; const hashtag = text.match(/^\s?#[^\s\.,]+/)[0];
const res: any[] = !isHead ? [{ const res: any[] = !isHead ? [{
type: 'text', type: 'text',
content: text[0] content: text[0]

View File

@ -8,13 +8,20 @@ export type TextElementQuote = {
quote: string quote: string
}; };
export default function(text: string) { export default function(text: string, index: number) {
const match = text.match(/^"([\s\S]+?)\n"/); const match = text.match(/^"([\s\S]+?)\n"/) || text.match(/^\n>([\s\S]+?)(\n\n|$)/) ||
(index == 0 ? text.match(/^>([\s\S]+?)(\n\n|$)/) : null);
if (!match) return null; if (!match) return null;
const quote = match[0];
const quote = match[1]
.split('\n')
.map(line => line.replace(/^>+/g, '').trim())
.join('\n');
return { return {
type: 'quote', type: 'quote',
content: quote, content: match[0],
quote: match[1].trim(), quote: quote,
} as TextElementQuote; } as TextElementQuote;
} }

View File

@ -2,12 +2,12 @@
* Replace fontawesome symbols * Replace fontawesome symbols
*/ */
import * as fontawesome from '@fortawesome/fontawesome'; import * as fontawesome from '@fortawesome/fontawesome-svg-core';
import regular from '@fortawesome/fontawesome-free-regular'; import { far } from '@fortawesome/free-regular-svg-icons';
import solid from '@fortawesome/fontawesome-free-solid'; import { fas } from '@fortawesome/free-solid-svg-icons';
import brands from '@fortawesome/fontawesome-free-brands'; import { fab } from '@fortawesome/free-brands-svg-icons';
fontawesome.library.add(regular, solid, brands); fontawesome.library.add(far, fas, fab);
export const pattern = /%fa:(.+?)%/g; export const pattern = /%fa:(.+?)%/g;

View File

@ -0,0 +1,21 @@
import * as mongo from 'mongodb';
function toString(id: any) {
return mongo.ObjectID.prototype.isPrototypeOf(id) ? (id as mongo.ObjectID).toHexString() : id;
}
export default function(note: any, mutedUserIds: string[]): boolean {
if (mutedUserIds.includes(toString(note.userId))) {
return true;
}
if (note.reply != null && mutedUserIds.includes(toString(note.reply.userId))) {
return true;
}
if (note.renote != null && mutedUserIds.includes(toString(note.renote.userId))) {
return true;
}
return false;
}

View File

@ -14,4 +14,5 @@ export type IMeta = {
disableRegistration?: boolean; disableRegistration?: boolean;
disableLocalTimeline?: boolean; disableLocalTimeline?: boolean;
hidedTags?: string[]; hidedTags?: string[];
bannerUrl?: string;
}; };

17
src/models/note-unread.ts Normal file
View File

@ -0,0 +1,17 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const NoteUnread = db.get<INoteUnread>('noteUnreads');
NoteUnread.createIndex(['userId', 'noteId'], { unique: true });
export default NoteUnread;
export interface INoteUnread {
_id: mongo.ObjectID;
noteId: mongo.ObjectID;
userId: mongo.ObjectID;
isSpecified: boolean;
_note: {
userId: mongo.ObjectID;
};
}

View File

@ -17,6 +17,8 @@ import Following from './following';
const Note = db.get<INote>('notes'); const Note = db.get<INote>('notes');
Note.createIndex('uri', { sparse: true, unique: true }); Note.createIndex('uri', { sparse: true, unique: true });
Note.createIndex('userId'); Note.createIndex('userId');
Note.createIndex('mentions');
Note.createIndex('visibleUserIds');
Note.createIndex('tagsLower'); Note.createIndex('tagsLower');
Note.createIndex('_files.contentType'); Note.createIndex('_files.contentType');
Note.createIndex({ Note.createIndex({
@ -24,6 +26,21 @@ Note.createIndex({
}); });
export default Note; export default Note;
// 後方互換性のため
Note.findOne({
fileIds: { $exists: true }
}).then(n => {
if (n == null) {
Note.update({}, {
$rename: {
mediaIds: 'fileIds'
}
}, {
multi: true
});
}
});
export function isValidText(text: string): boolean { export function isValidText(text: string): boolean {
return length(text.trim()) <= 1000 && text.trim() != ''; return length(text.trim()) <= 1000 && text.trim() != '';
} }
@ -179,7 +196,7 @@ export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => {
hide = false; hide = false;
} else { } else {
// 指定されているかどうか // 指定されているかどうか
const specified = packedNote.visibleUserIds.some((id: mongo.ObjectID) => id.equals(meId)); const specified = packedNote.visibleUserIds.some((id: any) => meId.equals(id));
if (specified) { if (specified) {
hide = false; hide = false;
@ -278,8 +295,8 @@ export const pack = async (
delete _note._user; delete _note._user;
delete _note._reply; delete _note._reply;
delete _note.repost; delete _note._renote;
delete _note.mentions; delete _note._files;
if (_note.geo) delete _note.geo.type; if (_note.geo) delete _note.geo.type;
// Populate user // Populate user

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