Compare commits

...

118 Commits

Author SHA1 Message Date
syuilo
ccfd48232a 8.51.0 2018-09-18 13:14:42 +09:00
syuilo
429bf179dc Refactor: Better type annotations 2018-09-18 13:14:17 +09:00
syuilo
8ba3fb13eb Fix bug 2018-09-18 13:12:41 +09:00
MeiMei
11496d887e Publish pinned notes (#2731) 2018-09-18 13:08:27 +09:00
syuilo
bec48319ec 8.50.0 2018-09-18 12:43:24 +09:00
syuilo
71a93b2b43 Refactor & Usability improvements 2018-09-18 12:42:56 +09:00
syuilo
6ed3f9e414 リファクタリングなど 2018-09-18 12:34:41 +09:00
syuilo
dc8f592c1f 8.49.0 2018-09-18 09:21:02 +09:00
syuilo
f66c31c771 Improve usability & refactoring 2018-09-18 09:20:06 +09:00
syuilo
55e2ae1408 Improve usability 2018-09-18 09:11:52 +09:00
syuilo
19c72627fc Improve keyboard shortcut 2018-09-18 08:19:45 +09:00
syuilo
2a4c53c3a4 8.48.0 2018-09-18 06:30:52 +09:00
syuilo
1f2ebce8ed Resolve #1302 2018-09-18 06:29:47 +09:00
syuilo
fcea9dacb7 Clean up: Remove unused import 2018-09-18 06:20:49 +09:00
syuilo
908872f374 8.47.0 2018-09-18 05:36:14 +09:00
syuilo
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
syuilo
b47b5d6d8b Merge pull request #2728 from syuilo/l10n_develop
New Crowdin translations
2018-09-18 05:35:39 +09:00
syuilo
31ce3aa312 キーボードショートカットを強化するなど 2018-09-18 05:35:06 +09:00
syuilo
5b22d92e99 New translations ja-JP.yml (English) 2018-09-18 03:51:42 +09:00
greenkeeper[bot]
df148e25da fix(package): update @types/node to version 10.10.1 2018-09-17 17:24:15 +00:00
syuilo
4b26df5c3a New translations ja-JP.yml (English) 2018-09-18 02:19:32 +09:00
syuilo
e765be4205 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-09-18 02:15:36 +09:00
syuilo
f7d2457063 New translations ja-JP.yml (Norwegian) 2018-09-18 02:15:27 +09:00
syuilo
6032d803aa New translations ja-JP.yml (Dutch) 2018-09-18 02:15:25 +09:00
syuilo
0de371db38 New translations ja-JP.yml (Japanese, Kansai) 2018-09-18 02:15:22 +09:00
syuilo
ce3797c4af 8.46.0 2018-09-18 02:15:19 +09:00
syuilo
56dd8c298b New translations ja-JP.yml (Spanish) 2018-09-18 02:15:19 +09:00
syuilo
3533257efe New translations ja-JP.yml (Russian) 2018-09-18 02:15:16 +09:00
syuilo
dc2f08721d New translations ja-JP.yml (Portuguese) 2018-09-18 02:15:14 +09:00
syuilo
66608a4131 New translations ja-JP.yml (Polish) 2018-09-18 02:15:11 +09:00
syuilo
2fa90131eb New translations ja-JP.yml (Korean) 2018-09-18 02:15:08 +09:00
syuilo
a51ed28db6 New translations ja-JP.yml (Italian) 2018-09-18 02:15:06 +09:00
syuilo
5ec290663b New translations ja-JP.yml (German) 2018-09-18 02:15:03 +09:00
syuilo
1374d6e34d New translations ja-JP.yml (French) 2018-09-18 02:15:00 +09:00
syuilo
45ade17c58 New translations ja-JP.yml (English) 2018-09-18 02:14:57 +09:00
syuilo
c753e26187 New translations ja-JP.yml (Chinese Simplified) 2018-09-18 02:14:54 +09:00
syuilo
577929eed1 New translations ja-JP.yml (Catalan) 2018-09-18 02:14:51 +09:00
syuilo
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
syuilo
77e53cbf9e Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-09-18 02:14:16 +09:00
syuilo
ab83e08bc7 メッセージタイムラインを追加 2018-09-18 02:14:12 +09:00
syuilo
2fad6e6d5f Refactor 2018-09-18 02:13:42 +09:00
syuilo
a3604a6c95 Merge pull request #2722 from syuilo/l10n_develop
New Crowdin translations
2018-09-17 23:12:54 +09:00
syuilo
44f6fe6f1f Refactor: Extract shouldMuteThisNote function 2018-09-17 23:07:15 +09:00
syuilo
311b4e90ca No lint when test 2018-09-17 22:51:25 +09:00
syuilo
f5a937c523 Better hashtag parsing 2018-09-17 22:51:10 +09:00
greenkeeper[bot]
0632a3ed3f fix(package): update @types/node to version 10.10.0 2018-09-17 08:10:08 +00:00
syuilo
71bada97df 8.45.1 2018-09-17 12:19:54 +09:00
syuilo
62509edcbe Refactor 2018-09-17 12:18:59 +09:00
syuilo
f97cdfaa20 Fix #2725 2018-09-17 11:59:24 +09:00
syuilo
67ec10e86d Add untilId param 2018-09-17 11:43:53 +09:00
syuilo
481b3f2c58 New translations ja-JP.yml (English) 2018-09-17 09:11:27 +09:00
syuilo
7d599a68ea pong 2018-09-17 09:07:46 +09:00
syuilo
7ccff732b8 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-09-17 09:07:05 +09:00
syuilo
7587c896d5 8.45.0 2018-09-17 09:06:56 +09:00
syuilo
91297f1ab3 Merge pull request #2717 from syuilo/l10n_develop
New Crowdin translations
2018-09-17 09:06:25 +09:00
syuilo
d872a16fe0 🎨 2018-09-17 09:05:51 +09:00
syuilo
60aa35adf8 New translations ja-JP.yml (Norwegian) 2018-09-17 09:01:59 +09:00
syuilo
5035b66773 New translations ja-JP.yml (Dutch) 2018-09-17 09:01:57 +09:00
syuilo
fa9da8ecab New translations ja-JP.yml (Japanese, Kansai) 2018-09-17 09:01:54 +09:00
syuilo
1f9bca7188 New translations ja-JP.yml (Spanish) 2018-09-17 09:01:52 +09:00
syuilo
ffa5bdeb50 New translations ja-JP.yml (Russian) 2018-09-17 09:01:49 +09:00
syuilo
e6bfb7398e New translations ja-JP.yml (Portuguese) 2018-09-17 09:01:47 +09:00
syuilo
6def0c776f New translations ja-JP.yml (Polish) 2018-09-17 09:01:45 +09:00
syuilo
24bae9eaed New translations ja-JP.yml (Korean) 2018-09-17 09:01:43 +09:00
syuilo
fb5175a283 New translations ja-JP.yml (Italian) 2018-09-17 09:01:40 +09:00
syuilo
6e49437154 New translations ja-JP.yml (German) 2018-09-17 09:01:38 +09:00
syuilo
2511ed56ac New translations ja-JP.yml (French) 2018-09-17 09:01:35 +09:00
syuilo
c4bfc99cf5 New translations ja-JP.yml (English) 2018-09-17 09:01:33 +09:00
syuilo
4efe38440d New translations ja-JP.yml (Chinese Simplified) 2018-09-17 09:01:30 +09:00
syuilo
4a5f2c3c40 New translations ja-JP.yml (Catalan) 2018-09-17 09:01:27 +09:00
syuilo
109738ccb9 ハッシュタグタイムラインを実装 2018-09-17 09:00:20 +09:00
syuilo
433dbe179d 8.44.1 2018-09-17 03:37:22 +09:00
syuilo
b21b33831a Fix bug 2018-09-17 03:36:58 +09:00
syuilo
020cc471da 8.44.0 2018-09-17 03:03:58 +09:00
xps2
43b47c4494 fontawesomeを5.3.1にアップデート (#2718) 2018-09-17 03:02:58 +09:00
syuilo
8751d91794 Better stats page 2018-09-17 02:56:57 +09:00
syuilo
374b276f5c Fix #2720 2018-09-17 02:45:30 +09:00
syuilo
6138a74231 Fix #2101 2018-09-17 00:20:00 +09:00
syuilo
25438c4d64 New translations ja-JP.yml (French) 2018-09-17 00:01:10 +09:00
syuilo
ae6ce19886 8.43.0 2018-09-16 23:19:06 +09:00
syuilo
e17a9bfd6f Merge pull request #2714 from syuilo/l10n_develop
New Crowdin translations
2018-09-16 23:17:22 +09:00
syuilo
dc2055f5bc Fix bug 2018-09-16 23:15:15 +09:00
syuilo
afeb8058b1 Add mentions column (Deck) 2018-09-16 23:15:02 +09:00
syuilo
9299f99ac3 New translations ja-JP.yml (English) 2018-09-16 23:02:02 +09:00
syuilo
858fc7ebcc New translations ja-JP.yml (Norwegian) 2018-09-16 22:51:36 +09:00
syuilo
35089c65d3 New translations ja-JP.yml (Dutch) 2018-09-16 22:51:34 +09:00
syuilo
643ca42829 New translations ja-JP.yml (Japanese, Kansai) 2018-09-16 22:51:31 +09:00
syuilo
935dc4fe33 New translations ja-JP.yml (Spanish) 2018-09-16 22:51:29 +09:00
syuilo
3a9e74feb1 New translations ja-JP.yml (Russian) 2018-09-16 22:51:27 +09:00
syuilo
92e66fbf0c New translations ja-JP.yml (Portuguese) 2018-09-16 22:51:24 +09:00
syuilo
a50515f569 New translations ja-JP.yml (Polish) 2018-09-16 22:51:22 +09:00
syuilo
2f8f47acea New translations ja-JP.yml (Korean) 2018-09-16 22:51:19 +09:00
syuilo
dcb296db93 New translations ja-JP.yml (Italian) 2018-09-16 22:51:17 +09:00
syuilo
0bdae9ede7 New translations ja-JP.yml (German) 2018-09-16 22:51:15 +09:00
syuilo
11290c2a0f New translations ja-JP.yml (French) 2018-09-16 22:51:13 +09:00
syuilo
428b8f8669 New translations ja-JP.yml (English) 2018-09-16 22:51:10 +09:00
syuilo
7ced10f84e New translations ja-JP.yml (Chinese Simplified) 2018-09-16 22:51:08 +09:00
syuilo
8ac54139c9 New translations ja-JP.yml (Catalan) 2018-09-16 22:51:05 +09:00
syuilo
32afe77a26 自分宛ての投稿をタイムラインで見れるように 2018-09-16 22:48:57 +09:00
syuilo
6db8e33662 New translations ja-JP.yml (English) 2018-09-16 22:01:26 +09:00
syuilo
569561f247 New translations ja-JP.yml (Norwegian) 2018-09-16 21:52:05 +09:00
syuilo
d132d82acf New translations ja-JP.yml (Dutch) 2018-09-16 21:52:02 +09:00
syuilo
9ba0db9372 New translations ja-JP.yml (Japanese, Kansai) 2018-09-16 21:51:59 +09:00
syuilo
5d468b542d New translations ja-JP.yml (Spanish) 2018-09-16 21:51:57 +09:00
syuilo
32273165c7 New translations ja-JP.yml (Russian) 2018-09-16 21:51:55 +09:00
syuilo
46fdb75bf4 New translations ja-JP.yml (Portuguese) 2018-09-16 21:51:52 +09:00
syuilo
baf381814b New translations ja-JP.yml (Polish) 2018-09-16 21:51:50 +09:00
syuilo
e90387c14e New translations ja-JP.yml (Korean) 2018-09-16 21:51:48 +09:00
syuilo
876790d499 New translations ja-JP.yml (Italian) 2018-09-16 21:51:45 +09:00
syuilo
8b56edda4b New translations ja-JP.yml (German) 2018-09-16 21:51:43 +09:00
syuilo
33352256d6 New translations ja-JP.yml (French) 2018-09-16 21:51:41 +09:00
syuilo
e368ef11fa New translations ja-JP.yml (English) 2018-09-16 21:51:39 +09:00
syuilo
045f7c3185 New translations ja-JP.yml (Chinese Simplified) 2018-09-16 21:51:36 +09:00
syuilo
bf40e5a5c5 New translations ja-JP.yml (Catalan) 2018-09-16 21:51:34 +09:00
syuilo
cda3635d97 enable-animations --> reduce-motion 2018-09-16 21:40:48 +09:00
syuilo
2eb561f132 New translations ja-JP.yml (Portuguese) 2018-09-16 05:43:02 +09:00
syuilo
b5f6465d61 New translations ja-JP.yml (Portuguese) 2018-09-16 05:31:03 +09:00
syuilo
9725076c46 New translations ja-JP.yml (Portuguese) 2018-09-16 05:20:49 +09:00
97 changed files with 1986 additions and 497 deletions

View File

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

View File

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

View File

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

View File

@@ -110,9 +110,9 @@ common:
verified-user: "Verified account"
disable-animated-mfm: "Disable animated texts in a post"
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"
enable-animations: "Enable animations"
reduce-motion: "Reduce motion in UI"
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.'
reversi:
@@ -155,7 +155,10 @@ common:
home: "Home"
local: "Local"
hybrid: "Social"
hashtag: "Hashtag"
global: "Global"
mentions: "Mentions"
direct: "ダイレクト投稿"
notifications: "Notifications"
list: "Lists"
swap-left: "Move to the left"
@@ -807,7 +810,13 @@ desktop/views/components/timeline.vue:
local: "Local"
hybrid: "Social"
global: "Global"
mentions: "Mentions"
messages: "Messages"
list: "Lists"
hashtag: "Hashtag"
add-tag-timeline: "Add hashtag tl"
add-list: "Add list"
list-name: "List name"
desktop/views/components/ui.header.vue:
welcome-back: "Welcome back,"
adjective: "-san"
@@ -1132,6 +1141,8 @@ mobile/views/pages/home.vue:
local: "Local"
hybrid: "Social"
global: "Global"
mentions: "Mentions"
messages: "Messages"
mobile/views/pages/tag.vue:
no-posts-found: "No posts \"{}\" found."
mobile/views/pages/welcome.vue:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "8.42.0",
"clientVersion": "1.0.9769",
"version": "8.51.0",
"clientVersion": "1.0.9887",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,
@@ -20,10 +20,10 @@
"format": "gulp format"
},
"dependencies": {
"@fortawesome/fontawesome": "1.1.8",
"@fortawesome/fontawesome-free-brands": "5.0.13",
"@fortawesome/fontawesome-free-regular": "5.0.13",
"@fortawesome/fontawesome-free-solid": "5.0.13",
"@fortawesome/fontawesome-svg-core": "1.2.4",
"@fortawesome/free-brands-svg-icons": "5.3.1",
"@fortawesome/free-regular-svg-icons": "5.3.1",
"@fortawesome/free-solid-svg-icons": "5.3.1",
"@koa/cors": "2.2.2",
"@prezzemolo/rap": "0.1.2",
"@prezzemolo/zip": "0.0.3",
@@ -60,7 +60,7 @@
"@types/mocha": "5.2.3",
"@types/mongodb": "3.1.7",
"@types/ms": "0.7.30",
"@types/node": "10.9.4",
"@types/node": "10.10.1",
"@types/portscanner": "2.1.0",
"@types/pug": "2.0.4",
"@types/qrcode": "1.2.0",

View File

@@ -0,0 +1,106 @@
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: []
} 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 => {
let matched = pattern.which.includes(key);
if (pattern.ctrl && !e.ctrlKey) matched = false;
if (pattern.shift && !e.shiftKey) matched = false;
if (pattern.alt && !e.altKey) matched = false;
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

@@ -1,9 +1,9 @@
<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="popover" :class="{ compact, big }" ref="popover">
<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('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>
@@ -31,30 +31,84 @@ export default Vue.extend({
type: Object,
required: true
},
source: {
required: true
},
compact: {
type: Boolean,
required: false,
default: false
},
cb: {
required: false
},
big: {
type: Boolean,
required: false,
default: false
},
showFocus: {
type: Boolean,
required: false,
default: false
},
animation: {
type: Boolean,
required: false,
default: true
}
},
data() {
return {
title: placeholder
title: placeholder,
focus: null
};
},
computed: {
keymap(): any {
return {
'esc': this.close,
'enter|space|plus': this.choose,
'up': this.focusUp,
'right': this.focusRight,
'down': this.focusDown,
'left': this.focusLeft,
'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.childNodes[i].focus();
if (this.showFocus) {
this.title = this.$refs.buttons.childNodes[i].title;
}
}
},
mounted() {
this.$nextTick(() => {
this.focus = 0;
const popover = this.$refs.popover as any;
const rect = this.source.getBoundingClientRect();
@@ -76,7 +130,7 @@ export default Vue.extend({
anime({
targets: this.$refs.backdrop,
opacity: 1,
duration: 100,
duration: this.animation ? 100 : 0,
easing: 'linear'
});
@@ -84,10 +138,11 @@ export default Vue.extend({
targets: this.$refs.popover,
opacity: 1,
scale: [0.5, 1],
duration: 500
duration: this.animation ? 500 : 0
});
});
},
methods: {
react(reaction) {
(this as any).api('notes/reactions/create', {
@@ -95,21 +150,25 @@ export default Vue.extend({
reaction: reaction
}).then(() => {
if (this.cb) this.cb();
this.$emit('closed');
this.destroyDom();
});
},
onMouseover(e) {
this.title = e.target.title;
},
onMouseout(e) {
this.title = placeholder;
},
close() {
(this.$refs.backdrop as any).style.pointerEvents = 'none';
anime({
targets: this.$refs.backdrop,
opacity: 0,
duration: 200,
duration: this.animation ? 200 : 0,
easing: 'linear'
});
@@ -118,10 +177,33 @@ export default Vue.extend({
targets: this.$refs.popover,
opacity: 0,
scale: 0.5,
duration: 200,
duration: this.animation ? 200 : 0,
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
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
padding 0
width 40px

View File

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

View File

@@ -1,5 +1,5 @@
<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 v-html="title" :class="$style.title"></span>
<span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}%i18n:@choose-file%)</span>

View File

@@ -1,5 +1,5 @@
<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 v-html="title" :class="$style.title"></span>
</span>

View File

@@ -1,5 +1,5 @@
<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">
<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:@used%</p>
<span :class="$style.title">%fa:cloud%%i18n:@drive%</span>

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<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>
<mk-reversi :class="$style.content" @gamed="g => game = g"/>
</mk-window>

View File

@@ -237,6 +237,10 @@ export default Vue.extend({
warp(date) {
(this.$refs.tl as any).warp(date);
},
focus() {
(this.$refs.tl as any).focus();
}
}
});

View File

@@ -1,5 +1,5 @@
<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">
%fa:i-cursor%{{ title }}
</span>

View File

@@ -1,5 +1,5 @@
<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>
<mk-messaging-room :user="user" :class="$style.content"/>
</mk-window>

View File

@@ -1,5 +1,5 @@
<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>
<mk-messaging :class="$style.content" @navigate="navigate"/>
</mk-window>

View File

@@ -1,5 +1,5 @@
<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)">
<x-sub :note="p.reply"/>
</div>
@@ -48,7 +48,7 @@
<button class="renoteButton" @click="renote" title="%i18n:@renote%">
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
</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>
</button>
<button @click="menu" ref="menuButton">
@@ -111,6 +111,16 @@ export default Vue.extend({
},
computed: {
keymap(): any {
return {
'r|left': this.reply,
'a|plus': () => this.react(true),
'n|right': this.renote,
'up|shift+tab': this.focusBefore,
'down|tab': this.focusAfter,
};
},
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
@@ -223,64 +233,46 @@ export default Vue.extend({
reply() {
(this as any).os.new(MkPostFormWindow, {
reply: this.p
});
}).$once('closed', this.focus);
},
renote() {
(this as any).os.new(MkRenoteFormWindow, {
note: this.p
});
}).$once('closed', this.focus);
},
react() {
react(viaKeyboard = false) {
this.blur();
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.p
});
note: this.p,
showFocus: viaKeyboard,
animation: !viaKeyboard
}).$once('closed', this.focus);
},
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
note: this.p
});
}).$once('closed', this.focus);
},
onKeydown(e) {
let shouldBeCancel = true;
focus() {
this.$el.focus();
},
switch (true) {
case e.which == 38: // [↑]
case e.which == 74: // [j]
case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
focus(this.$el, e => e.previousElementSibling);
break;
blur() {
this.$el.blur();
},
case e.which == 40: // [↓]
case e.which == 75: // [k]
case e.which == 9: // [Tab]
focus(this.$el, e => e.nextElementSibling);
break;
focusBefore() {
focus(this.$el, e => e.previousElementSibling);
},
case e.which == 81: // [q]
case e.which == 69: // [e]
this.renote();
break;
case e.which == 70: // [f]
case e.which == 76: // [l]
//this.like();
break;
case e.which == 82: // [r]
this.reply();
break;
default:
shouldBeCancel = false;
}
if (shouldBeCancel) e.preventDefault();
focusAfter() {
focus(this.$el, e => e.nextElementSibling);
}
}
});

View File

@@ -10,9 +10,9 @@
</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">
<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">
<span>%fa:angle-up%{{ note._datetext }}</span>
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
@@ -89,7 +89,7 @@ export default Vue.extend({
},
focus() {
(this.$el as any).children[0].focus();
(this.$refs.note as any)[0].focus();
},
onNoteUpdated(i, note) {

View File

@@ -2,7 +2,7 @@
<div class="mk-notifications">
<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">
<div class="notification" :class="notification.type" :key="notification.id">
<mk-time :time="notification.createdAt"/>

View File

@@ -1,5 +1,5 @@
<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">
<span slot="header" class="mk-post-form-window--header">
<span class="icon" v-if="geo">%fa:map-marker-alt%</span>
<span v-if="!reply">%i18n:@note%</span>
@@ -53,6 +53,10 @@ export default Vue.extend({
},
onPosted() {
(this.$refs.window as any).close();
},
onWindowClosed() {
this.$emit('closed');
this.destroyDom();
}
}
});

View File

@@ -1,5 +1,5 @@
<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>
<div :class="$style.body">
<p :class="$style.init" v-if="isNaN(value)">%i18n:@waiting%<mk-ellipsis/></p>

View File

@@ -1,5 +1,5 @@
<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>
<div class="slpqaxdoxhvglersgjukmvizkqbmbokc" :data-darkmode="$store.state.device.darkmode">

View File

@@ -1,7 +1,7 @@
<template>
<mk-window ref="window" is-modal @closed="$destroy">
<mk-window ref="window" is-modal @closed="onWindowClosed">
<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>
</template>
@@ -10,25 +10,32 @@ import Vue from 'vue';
export default Vue.extend({
props: ['note'],
mounted() {
document.addEventListener('keydown', this.onDocumentKeydown);
},
beforeDestroy() {
document.removeEventListener('keydown', this.onDocumentKeydown);
computed: {
keymap(): any {
return {
'esc': this.close,
'ctrl+enter': this.post
};
}
},
methods: {
onDocumentKeydown(e) {
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
if (e.which == 27) { // Esc
(this.$refs.window as any).close();
}
}
post() {
(this.$refs.form as any).ok();
},
close() {
(this.$refs.window as any).close();
},
onPosted() {
(this.$refs.window as any).close();
},
onCanceled() {
(this.$refs.window as any).close();
},
onWindowClosed() {
this.$emit('closed');
this.destroyDom();
}
}
});

View File

@@ -1,13 +1,19 @@
<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>
<mk-settings @done="close"/>
<mk-settings :initial-page="initialPage" @done="close"/>
</mk-window>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
initialPage: {
type: String,
required: false
}
},
methods: {
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 == '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 == '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 == '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>
@@ -60,7 +61,7 @@
<button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button>
<mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/>
<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="showFullAcct" text="%i18n:common.show-full-acct%"/>
<mk-switch v-model="gradientWindowHeader" text="%i18n:@gradient-window-header%"/>
@@ -138,6 +139,11 @@
<x-drive/>
</section>
<section class="hashtags" v-show="page == 'hashtags'">
<h1>%i18n:@tags%</h1>
<x-tags/>
</section>
<section class="mute" v-show="page == 'mute'">
<h1>%i18n:@mute%</h1>
<x-mute/>
@@ -222,6 +228,7 @@ import XApi from './settings.api.vue';
import XApps from './settings.apps.vue';
import XSignins from './settings.signins.vue';
import XDrive from './settings.drive.vue';
import XTags from './settings.tags.vue';
import { url, langs, version } from '../../../config';
import checkForUpdate from '../../../common/scripts/check-for-update';
@@ -234,11 +241,18 @@ export default Vue.extend({
XApi,
XApps,
XSignins,
XDrive
XDrive,
XTags
},
props: {
initialPage: {
type: String,
required: false
}
},
data() {
return {
page: 'profile',
page: this.initialPage || 'profile',
meta: null,
version,
langs,
@@ -247,9 +261,9 @@ export default Vue.extend({
};
},
computed: {
animations: {
get() { return this.$store.state.device.animations; },
set(value) { this.$store.commit('device/set', { key: 'animations', value }); }
reduceMotion: {
get() { return this.$store.state.device.reduceMotion; },
set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
},
apiViaStream: {

View File

@@ -15,6 +15,7 @@
<script lang="ts">
import Vue from 'vue';
import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
const fetchLimit = 10;
@@ -23,6 +24,9 @@ export default Vue.extend({
src: {
type: String,
required: true
},
tagTl: {
required: false
}
},
@@ -31,9 +35,17 @@ export default Vue.extend({
fetching: true,
moreFetching: false,
existMore: false,
streamManager: null,
connection: 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;
},
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 {
return !this.moreFetching && !this.fetching && this.existMore;
}
},
mounted() {
this.connection = this.stream.getConnection();
this.connectionId = this.stream.use();
const prepend = note => {
(this.$refs.timeline as any).prepend(note);
};
this.connection.on('note', this.onNote);
if (this.src == 'home') {
this.connection.on('follow', this.onChangeFollowing);
this.connection.on('unfollow', this.onChangeFollowing);
if (this.src == 'tag') {
this.endpoint = 'notes/search_by_tag';
this.query = {
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();
},
beforeDestroy() {
this.connection.off('note', this.onNote);
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);
this.$emit('beforeDestroy');
},
methods: {
@@ -96,13 +164,10 @@ export default Vue.extend({
this.fetching = true;
(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,
untilDate: this.date ? this.date.getTime() : undefined,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
}).then(notes => {
untilDate: this.date ? this.date.getTime() : undefined
}, this.baseQuery, this.query)).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
@@ -119,13 +184,10 @@ export default Vue.extend({
this.moreFetching = true;
const promise = (this as any).api(this.endpoint, {
const promise = (this as any).api(this.endpoint, Object.assign({
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
});
untilId: (this.$refs.timeline as any).tail().id
}, this.baseQuery, this.query));
promise.then(notes => {
if (notes.length == fetchLimit + 1) {
@@ -140,15 +202,6 @@ export default Vue.extend({
return promise;
},
onNote(note) {
// Prepend a note
(this.$refs.timeline as any).prepend(note);
},
onChangeFollowing() {
this.fetch();
},
focus() {
(this.$refs.timeline as any).focus();
},
@@ -156,14 +209,6 @@ export default Vue.extend({
warp(date) {
this.date = date;
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 == '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 == '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>
<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%</button>
<button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%">%fa:envelope R%</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>
<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 == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
<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"/>
</div>
</template>
@@ -19,7 +28,8 @@
<script lang="ts">
import Vue from '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({
components: {
@@ -30,6 +40,7 @@ export default Vue.extend({
return {
src: 'home',
list: null,
tagTl: null,
enableLocalTimeline: false
};
},
@@ -39,8 +50,14 @@ export default Vue.extend({
this.saveSrc();
},
list() {
list(x) {
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;
if (this.src == 'list') {
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) {
this.src = 'hybrid';
@@ -69,20 +88,86 @@ export default Vue.extend({
saveSrc() {
this.$store.commit('device/setTl', {
src: this.src,
arg: this.list
arg: this.src == 'list' ? this.list : this.tagTl
});
},
focus() {
(this.$refs.tl as any).focus();
},
warp(date) {
(this.$refs.tl as any).warp(date);
},
chooseList() {
const w = (this as any).os.new(MkUserListsWindow);
w.$once('choosen', list => {
this.list = list;
this.src = 'list';
w.close();
async chooseList() {
const lists = await (this as any).api('users/lists/list');
let menu = [{
icon: '%fa:plus%',
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,38 @@ root(isDark)
border-radius 6px 6px 0 0
box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
> button
> .buttons
position absolute
z-index 2
top 0
right 0
padding 0
width 42px
font-size 0.9em
line-height 42px
color isDark ? #9baec8 : #ccc
padding-right 8px
&:hover
color isDark ? #b2c1d5 : #aaa
> button
padding 0 8px
font-size 0.9em
line-height 42px
color isDark ? #9baec8 : #ccc
&:active
color isDark ? #b2c1d5 : #999
&: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
display inline-block

View File

@@ -1,5 +1,5 @@
<template>
<div class="mk-ui" :style="style">
<div class="mk-ui" :style="style" v-hotkey.global="keymap">
<x-header class="header" v-show="!zenMode"/>
<div class="content">
<slot></slot>
@@ -16,11 +16,13 @@ export default Vue.extend({
components: {
XHeader
},
data() {
return {
zenMode: false
};
},
computed: {
style(): any {
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,
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: {
onKeydown(e) {
if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return;
post() {
(this as any).apis.post();
},
if (e.which == 80 || e.which == 78) { // p or n
e.preventDefault();
(this as any).apis.post();
}
if (e.which == 90) { // z
e.preventDefault();
this.zenMode = !this.zenMode;
}
toggleZenMode() {
this.zenMode = !this.zenMode;
}
}
});

View File

@@ -1,5 +1,5 @@
<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>
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp" :data-darkmode="$store.state.device.darkmode">

View File

@@ -190,8 +190,8 @@ export default Vue.extend({
});
setTimeout(() => {
this.destroyDom();
this.$emit('closed');
this.destroyDom();
}, 300);
},

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 == '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 == '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>
<script lang="ts">
@@ -13,12 +16,16 @@ import Vue from 'vue';
import XTlColumn from './deck.tl-column.vue';
import XNotificationsColumn from './deck.notifications-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({
components: {
XTlColumn,
XNotificationsColumn,
XWidgetsColumn
XWidgetsColumn,
XMentionsColumn,
XDirectColumn
},
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

@@ -1,7 +1,7 @@
<template>
<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">
<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'">

View File

@@ -6,6 +6,7 @@
<template v-if="column.type == 'hybrid'">%fa:share-alt%</template>
<template v-if="column.type == 'global'">%fa:globe%</template>
<template v-if="column.type == 'list'">%fa:list%</template>
<template v-if="column.type == 'hashtag'">%fa:hashtag%</template>
<span>{{ name }}</span>
</span>
@@ -14,6 +15,7 @@
<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/>
</div>
<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-column>
</template>
@@ -23,12 +25,14 @@ import Vue from 'vue';
import XColumn from './deck.column.vue';
import XTl from './deck.tl.vue';
import XListTl from './deck.list-tl.vue';
import XHashtagTl from './deck.hashtag-tl.vue';
export default Vue.extend({
components: {
XColumn,
XTl,
XListTl
XListTl,
XHashtagTl
},
props: {
@@ -65,6 +69,7 @@ export default Vue.extend({
case 'hybrid': return '%i18n:common.deck.hybrid%';
case 'global': return '%i18n:common.deck.global%';
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'
});
}
}, {
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%',
text: '%i18n:common.deck.list%',
@@ -152,6 +170,20 @@ export default Vue.extend({
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%',
text: '%i18n:common.deck.notifications%',

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
<main>
<div class="main">
<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"/>
</div>
<div class="side">
@@ -28,7 +28,6 @@
<script lang="ts">
import Vue from 'vue';
import parseAcct from '../../../../../../misc/acct/parse';
import getUserName from '../../../../../../misc/get-user-name';
import Progress from '../../../../common/scripts/loading';
import XHeader from './user.header.vue';
import XTimeline from './user.timeline.vue';

View File

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

View File

@@ -14,7 +14,7 @@
</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">
<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">

View File

@@ -1,7 +1,7 @@
<template>
<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">
<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">

View File

@@ -13,6 +13,7 @@
<script lang="ts">
import Vue from 'vue';
import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
const fetchLimit = 10;
@@ -21,6 +22,9 @@ export default Vue.extend({
src: {
type: String,
required: true
},
tagTl: {
required: false
}
},
@@ -29,10 +33,18 @@ export default Vue.extend({
fetching: true,
moreFetching: false,
existMore: false,
streamManager: null,
connection: null,
connectionId: null,
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;
},
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 {
return !this.moreFetching && !this.fetching && this.existMore;
}
},
mounted() {
this.connection = this.stream.getConnection();
this.connectionId = this.stream.use();
const prepend = note => {
(this.$refs.timeline as any).prepend(note);
};
this.connection.on('note', this.onNote);
if (this.src == 'home') {
this.connection.on('follow', this.onChangeFollowing);
this.connection.on('unfollow', this.onChangeFollowing);
if (this.src == 'tag') {
this.endpoint = 'notes/search_by_tag';
this.query = {
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();
},
beforeDestroy() {
this.connection.off('note', this.onNote);
if (this.src == 'home') {
this.connection.off('follow', this.onChangeFollowing);
this.connection.off('unfollow', this.onChangeFollowing);
}
this.stream.dispose(this.connectionId);
this.$emit('beforeDestroy');
},
methods: {
@@ -91,13 +163,10 @@ export default Vue.extend({
this.fetching = true;
(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,
untilDate: this.date ? this.date.getTime() : undefined,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
}).then(notes => {
untilDate: this.date ? this.date.getTime() : undefined
}, this.baseQuery, this.query)).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
@@ -114,13 +183,10 @@ export default Vue.extend({
this.moreFetching = true;
const promise = (this as any).api(this.endpoint, {
const promise = (this as any).api(this.endpoint, Object.assign({
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
});
untilId: (this.$refs.timeline as any).tail().id
}, this.baseQuery, this.query));
promise.then(notes => {
if (notes.length == fetchLimit + 1) {
@@ -135,15 +201,6 @@ export default Vue.extend({
return promise;
},
onNote(note) {
// Prepend a note
(this.$refs.timeline as any).prepend(note);
},
onChangeFollowing() {
this.fetch();
},
focus() {
(this.$refs.timeline as any).focus();
},

View File

@@ -6,7 +6,10 @@
<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 == '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 == 'tag'">%fa:hashtag%{{ tagTl.title }}</span>
</span>
<span style="margin-left:8px">
<template v-if="!showNav">%fa:angle-down%</template>
@@ -21,15 +24,22 @@
<main :data-darkmode="$store.state.device.darkmode">
<div class="nav" v-if="showNav">
<div class="bg" @click="showNav = false"></div>
<div class="pointer"></div>
<div class="body">
<div>
<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 == '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>
<div class="hr"></div>
<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span>
<span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%</span>
<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>
</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>
@@ -39,6 +49,9 @@
<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 == '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"/>
</div>
</main>
@@ -60,6 +73,7 @@ export default Vue.extend({
src: 'home',
list: null,
lists: null,
tagTl: null,
showNav: false,
enableLocalTimeline: false
};
@@ -71,9 +85,16 @@ export default Vue.extend({
this.saveSrc();
},
list() {
list(x) {
this.showNav = false;
this.saveSrc();
if (x != null) this.tagTl = null;
},
tagTl(x) {
this.showNav = false;
this.saveSrc();
if (x != null) this.list = null;
},
showNav(v) {
@@ -94,6 +115,8 @@ export default Vue.extend({
this.src = this.$store.state.device.tl.src;
if (this.src == 'list') {
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) {
this.src = 'hybrid';
@@ -118,7 +141,7 @@ export default Vue.extend({
saveSrc() {
this.$store.commit('device/setTl', {
src: this.src,
arg: this.list
arg: this.src == 'list' ? this.list : this.tagTl
});
},
@@ -134,6 +157,26 @@ export default Vue.extend({
root(isDark)
> .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
position fixed
z-index 10000
@@ -150,28 +193,22 @@ root(isDark)
left 0
right 0
width 300px
max-height calc(100% - 70px)
margin 0 auto
overflow auto
-webkit-overflow-scrolling touch
background isDark ? #272f3a : #fff
border-radius 8px
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
padding 8px 0
> *
> .hr
margin 8px 0
border-top solid 1px isDark ? rgba(#000, 0.3) : rgba(#000, 0.1)
> *:not(.hr)
display block
padding 8px 16px
color isDark ? #cdd0d8 : #666

View File

@@ -13,7 +13,7 @@
<section>
<ui-switch v-model="darkmode">%i18n:@dark-mode%</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="showFullAcct">%i18n:common.show-full-acct%</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 }); }
},
animations: {
get() { return this.$store.state.device.animations; },
set(value) { this.$store.commit('device/set', { key: 'animations', value }); }
reduceMotion: {
get() { return this.$store.state.device.reduceMotion; },
set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
},
alwaysShowNsfw: {

View File

@@ -1,6 +1,6 @@
<template>
<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">
<h2>%fa:R comments%%i18n:@recent-notes%</h2>
<div>

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ html(lang= lang)
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")
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
body

View File

@@ -82,8 +82,12 @@ const handlers: { [key: string]: (window: any, token: any, mentionedRemoteUsers:
text({ document }, { content }) {
const nodes = (content as string).split('\n').map(x => document.createTextNode(x));
for (const x of intersperse(document.createElement('br'), nodes)) {
document.body.appendChild(x);
for (const x of intersperse('br', nodes)) {
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) {
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 hashtag = text.match(/^\s?#[^\s]+/)[0];
const hashtag = text.match(/^\s?#[^\s\.,]+/)[0];
const res: any[] = !isHead ? [{
type: 'text',
content: text[0]

View File

@@ -2,12 +2,12 @@
* Replace fontawesome symbols
*/
import * as fontawesome from '@fortawesome/fontawesome';
import regular from '@fortawesome/fontawesome-free-regular';
import solid from '@fortawesome/fontawesome-free-solid';
import brands from '@fortawesome/fontawesome-free-brands';
import * as fontawesome from '@fortawesome/fontawesome-svg-core';
import { far } from '@fortawesome/free-regular-svg-icons';
import { fas } from '@fortawesome/free-solid-svg-icons';
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;

View File

@@ -0,0 +1,15 @@
export default function(note: any, mutedUserIds: string[]): boolean {
if (mutedUserIds.indexOf(note.userId) != -1) {
return true;
}
if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
return true;
}
if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
return true;
}
return false;
}

View File

@@ -17,6 +17,8 @@ import Following from './following';
const Note = db.get<INote>('notes');
Note.createIndex('uri', { sparse: true, unique: true });
Note.createIndex('userId');
Note.createIndex('mentions');
Note.createIndex('visibleUserIds');
Note.createIndex('tagsLower');
Note.createIndex('_files.contentType');
Note.createIndex({
@@ -24,6 +26,21 @@ Note.createIndex({
});
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 {
return length(text.trim()) <= 1000 && text.trim() != '';
}
@@ -179,7 +196,7 @@ export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => {
hide = false;
} else {
// 指定されているかどうか
const specified = packedNote.visibleUserIds.some((id: mongo.ObjectID) => id.equals(meId));
const specified = packedNote.visibleUserIds.some((id: any) => meId.equals(id));
if (specified) {
hide = false;

View File

@@ -35,6 +35,28 @@ User.createIndex('uri', { sparse: true, unique: true });
export default User;
// 後方互換性のため
User.findOne({
pinnedNoteId: { $exists: true }
}).then(async x => {
if (x == null) return;
const users = await User.find({
pinnedNoteId: { $exists: true }
});
users.forEach(u => {
User.update({ _id: u._id }, {
$set: {
pinnedNoteIds: [(u as any).pinnedNoteId]
},
$unset: {
pinnedNoteId: ''
}
});
});
});
type IUserBase = {
_id: mongo.ObjectID;
createdAt: Date;
@@ -53,7 +75,7 @@ type IUserBase = {
wallpaperUrl?: string;
data: any;
description: string;
pinnedNoteId: mongo.ObjectID;
pinnedNoteIds: mongo.ObjectID[];
/**
* 凍結されているか否か
@@ -464,11 +486,11 @@ export const pack = (
}
if (opts.detail) {
if (_user.pinnedNoteId) {
// Populate pinned note
_user.pinnedNote = packNote(_user.pinnedNoteId, meId, {
if (_user.pinnedNoteIds) {
// Populate pinned notes
_user.pinnedNotes = Promise.all(_user.pinnedNoteIds.map((id: mongo.ObjectId) => packNote(id, meId, {
detail: true
});
})));
}
if (meId && !meId.equals(_user.id)) {

View File

@@ -1,22 +1,10 @@
import { INote } from '../../../models/note';
import toHtml from '../../../mfm/html';
import parse from '../../../mfm/parse';
import config from '../../../config';
export default function(note: INote) {
let html = toHtml(parse(note.text), note.mentionedRemoteUsers);
if (html == null) html = '';
if (note.poll != null) {
const url = `${config.url}/notes/${note._id}`;
// TODO: i18n
html += `<p><a href="${url}">【Misskeyで投票を見る】</a></p>`;
}
if (note.renoteId != null) {
const url = `${config.url}/notes/${note.renoteId}`;
html += `<p>RE: <a href="${url}">${url}</a></p>`;
}
return html;
}

View File

@@ -0,0 +1,9 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
export default (user: ILocalUser, target: any, object: any) => ({
type: 'Add',
actor: `${config.url}/users/${user._id}`,
target,
object
});

View File

@@ -6,6 +6,7 @@ import DriveFile, { IDriveFile } from '../../../models/drive-file';
import Note, { INote } from '../../../models/note';
import User from '../../../models/user';
import toHtml from '../misc/get-note-html';
import parseMfm from '../../../mfm/parse';
export default async function renderNote(note: INote, dive = true): Promise<any> {
const promisedFiles: Promise<IDriveFile[]> = note.fileIds
@@ -81,13 +82,39 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
const files = await promisedFiles;
let text = note.text;
if (note.poll != null) {
if (text == null) text = '';
const url = `${config.url}/notes/${note._id}`;
// TODO: i18n
text += `\n\n[投票を見る](${url})`;
}
if (note.renoteId != null) {
if (text == null) text = '';
const url = `${config.url}/notes/${note.renoteId}`;
text += `\n\nRE: ${url}`;
}
// 省略されたメンションのホストを復元する
if (text != null) {
text = parseMfm(text).map(x => {
if (x.type == 'mention' && x.host == null) {
return `${x.content}@${config.host}`;
} else {
return x.content;
}
}).join('');
}
return {
id: `${config.url}/notes/${note._id}`,
type: 'Note',
attributedTo,
summary: note.cw,
content: toHtml(note),
_misskey_content_: note.text,
content: toHtml(Object.assign({}, note, { text })),
_misskey_content: text,
published: note.createdAt.toISOString(),
to,
cc,

View File

@@ -4,8 +4,9 @@
* @param totalItems Total number of items
* @param first URL of first page (optional)
* @param last URL of last page (optional)
* @param orderedItems attached objects (optional)
*/
export default function(id: string, totalItems: any, first: string, last: string) {
export default function(id: string, totalItems: any, first?: string, last?: string, orderedItems?: object) {
const page: any = {
id,
type: 'OrderedCollection',
@@ -14,6 +15,7 @@ export default function(id: string, totalItems: any, first: string, last: string
if (first) page.first = first;
if (last) page.last = last;
if (orderedItems) page.orderedItems = orderedItems;
return page;
}

View File

@@ -21,6 +21,7 @@ export default async (user: ILocalUser) => {
outbox: `${id}/outbox`,
followers: `${id}/followers`,
following: `${id}/following`,
featured: `${id}/collections/featured`,
sharedInbox: `${config.url}/inbox`,
url: `${config.url}/@${user.username}`,
preferredUsername: user.username,

View File

@@ -0,0 +1,9 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
export default (user: ILocalUser, target: any, object: any) => ({
type: 'Remove',
actor: `${config.url}/users/${user._id}`,
target,
object
});

View File

@@ -53,6 +53,7 @@ export interface IPerson extends IObject {
publicKey: any;
followers: any;
following: any;
featured?: any;
outbox: any;
endpoints: string[];
}

View File

@@ -13,6 +13,7 @@ import renderPerson from '../remote/activitypub/renderer/person';
import Outbox, { packActivity } from './activitypub/outbox';
import Followers from './activitypub/followers';
import Following from './activitypub/following';
import Featured from './activitypub/featured';
// Init router
const router = new Router();
@@ -102,6 +103,9 @@ router.get('/users/:user/followers', Followers);
// following
router.get('/users/:user/following', Following);
// featured
router.get('/users/:user/collections/featured', Featured);
// publickey
router.get('/users/:user/publickey', async ctx => {
const userId = new mongo.ObjectID(ctx.params.user);

View File

@@ -0,0 +1,38 @@
import * as mongo from 'mongodb';
import * as Router from 'koa-router';
import config from '../../config';
import User from '../../models/user';
import pack from '../../remote/activitypub/renderer';
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
import { setResponseType } from '../activitypub';
import Note from '../../models/note';
import renderNote from '../../remote/activitypub/renderer/note';
export default async (ctx: Router.IRouterContext) => {
const userId = new mongo.ObjectID(ctx.params.user);
// Verify user
const user = await User.findOne({
_id: userId,
host: null
});
if (user === null) {
ctx.status = 404;
return;
}
const pinnedNoteIds = user.pinnedNoteIds || [];
const pinnedNotes = await Promise.all(pinnedNoteIds.map(id => Note.findOne({ _id: id })));
const renderedNotes = await Promise.all(pinnedNotes.map(note => renderNote(note)));
const rendered = renderOrderedCollection(
`${config.url}/users/${userId}/collections/featured`,
renderedNotes.length, null, null, renderedNotes
);
ctx.body = pack(rendered);
setResponseType(ctx);
};

View File

@@ -1,7 +1,9 @@
import * as mongo from 'mongodb';
import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import User, { ILocalUser } from '../../../../models/user';
import Note from '../../../../models/note';
import { pack } from '../../../../models/user';
import { deliverPinnedChange } from '../../../../services/i/pin';
/**
* Pin note
@@ -21,9 +23,25 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
return rej('note not found');
}
let addedId: mongo.ObjectID;
let removedId: mongo.ObjectID;
const pinnedNoteIds = user.pinnedNoteIds || [];
if (pinnedNoteIds.some(id => id.equals(note._id))) {
return rej('already exists');
}
pinnedNoteIds.unshift(note._id);
addedId = note._id;
if (pinnedNoteIds.length > 5) {
removedId = pinnedNoteIds.pop();
}
await User.update(user._id, {
$set: {
pinnedNoteId: note._id
pinnedNoteIds: pinnedNoteIds
}
});
@@ -32,6 +50,9 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
detail: true
});
// Send Add/Remove to followers
deliverPinnedChange(user._id, removedId, addedId);
// Send response
res(iObj);
});

View File

@@ -3,6 +3,7 @@ import Note from '../../../../models/note';
import { getFriendIds } from '../../common/get-friends';
import { pack } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params';
export const meta = {
desc: {
@@ -10,42 +11,55 @@ export const meta = {
'en-US': 'Get mentions of myself.'
},
requireCredential: true
requireCredential: true,
params: {
following: $.bool.optional.note({
default: false
}),
limit: $.num.optional.range(1, 100).note({
default: 10
}),
sinceId: $.type(ID).optional.note({
}),
untilId: $.type(ID).optional.note({
}),
visibility: $.str.optional.note({
}),
}
};
export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
// Get 'following' parameter
const [following = false, followingError] =
$.bool.optional.get(params.following);
if (followingError) return rej('invalid following param');
// Get 'limit' parameter
const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
if (untilIdErr) return rej('invalid untilId param');
const [ps, psErr] = getParams(meta, params);
if (psErr) throw psErr;
// Check if both of sinceId and untilId is specified
if (sinceId && untilId) {
if (ps.sinceId && ps.untilId) {
return rej('cannot set sinceId and untilId');
}
// Construct query
const query = {
mentions: user._id
$or: [{
mentions: user._id
}, {
visibleUserIds: user._id
}]
} as any;
const sort = {
_id: -1
};
if (following) {
if (ps.visibility) {
query.visibility = ps.visibility;
}
if (ps.following) {
const followingIds = await getFriendIds(user._id);
query.userId = {
@@ -53,26 +67,24 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
};
}
if (sinceId) {
if (ps.sinceId) {
sort._id = 1;
query._id = {
$gt: sinceId
$gt: ps.sinceId
};
} else if (untilId) {
} else if (ps.untilId) {
query._id = {
$lt: untilId
$lt: ps.untilId
};
}
// Issue query
const mentions = await Note
.find(query, {
limit: limit,
limit: ps.limit,
sort: sort
});
// Serialize
res(await Promise.all(mentions.map(async mention =>
await pack(mention, user)
)));
res(await Promise.all(mentions.map(mention => pack(mention, user))));
});

View File

@@ -13,12 +13,18 @@ export const meta = {
},
params: {
tag: $.str.note({
tag: $.str.optional.note({
desc: {
'ja-JP': 'タグ'
}
}),
query: $.arr($.arr($.str)).optional.note({
desc: {
'ja-JP': 'クエリ'
}
}),
includeUserIds: $.arr($.type(ID)).optional.note({
default: []
}),
@@ -59,11 +65,9 @@ export const meta = {
}
}),
withFiles: $.bool.optional.nullable.note({
default: null,
withFiles: $.bool.optional.note({
desc: {
'ja-JP': 'ファイルが添付された投稿に限定するか否か'
'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
}
}),
@@ -83,6 +87,12 @@ export const meta = {
}
}),
untilId: $.type(ID).optional.note({
desc: {
'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
}
}),
sinceDate: $.num.optional.note({
}),
@@ -126,8 +136,14 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}
const q: any = {
$and: [{
$and: [ps.tag ? {
tagsLower: ps.tag.toLowerCase()
} : {
$or: ps.query.map(tags => ({
$and: tags.map(t => ({
tagsLower: t.toLowerCase()
}))
}))
}],
deletedAt: { $exists: false }
};
@@ -281,25 +297,10 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
const withFiles = ps.withFiles != null ? ps.withFiles : ps.media;
if (withFiles != null) {
if (withFiles) {
push({
fileIds: {
$exists: true,
$ne: null
}
});
} else {
push({
$or: [{
fileIds: {
$exists: false
}
}, {
fileIds: null
}]
});
}
if (withFiles) {
push({
fileIds: { $exists: true, $ne: [] }
});
}
if (ps.poll != null) {
@@ -323,6 +324,14 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}
}
if (ps.untilId) {
push({
_id: {
$lt: ps.untilId
}
});
}
if (ps.sinceDate) {
push({
createdAt: {

View File

@@ -3,6 +3,7 @@ import Xev from 'xev';
import { IUser } from '../../../models/user';
import Mute from '../../../models/mute';
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
export default async function(
request: websocket.request,
@@ -15,17 +16,8 @@ export default async function(
// Subscribe stream
subscriber.on('global-timeline', async note => {
//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (mutedUserIds.indexOf(note.userId) != -1) {
return;
}
if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
return;
}
if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
return;
}
//#endregion
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, mutedUserIds)) return;
connection.send(JSON.stringify({
type: 'note',

View File

@@ -0,0 +1,40 @@
import * as websocket from 'websocket';
import Xev from 'xev';
import { IUser } from '../../../models/user';
import Mute from '../../../models/mute';
import { pack } from '../../../models/note';
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
export default async function(
request: websocket.request,
connection: websocket.connection,
subscriber: Xev,
user?: IUser
) {
const mute = user ? await Mute.find({ muterId: user._id }) : null;
const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
const q: Array<string[]> = JSON.parse((request.resourceURL.query as any).q);
// Subscribe stream
subscriber.on('hashtag', async note => {
const matched = q.some(tags => tags.every(tag => note.tags.map((t: string) => t.toLowerCase()).includes(tag.toLowerCase())));
if (!matched) return;
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, user, {
detail: true
});
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, mutedUserIds)) return;
connection.send(JSON.stringify({
type: 'note',
body: note
}));
});
}

View File

@@ -8,6 +8,7 @@ import { pack as packNote, pack } from '../../../models/note';
import readNotification from '../common/read-notification';
import call from '../call';
import { IApp } from '../../../models/app';
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
const log = debug('misskey');
@@ -45,15 +46,7 @@ export default async function(
//#region 流れてきたメッセージがミュートしているユーザーが関わるものだったら無視する
if (x.type == 'note') {
if (mutedUserIds.includes(x.body.userId)) {
return;
}
if (x.body.reply != null && mutedUserIds.includes(x.body.reply.userId)) {
return;
}
if (x.body.renote != null && mutedUserIds.includes(x.body.renote.userId)) {
return;
}
if (shouldMuteThisNote(x.body, mutedUserIds)) return;
} else if (x.type == 'notification') {
if (mutedUserIds.includes(x.body.userId)) {
return;

View File

@@ -4,6 +4,7 @@ import Xev from 'xev';
import { IUser } from '../../../models/user';
import Mute from '../../../models/mute';
import { pack } from '../../../models/note';
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
export default async function(
request: websocket.request,
@@ -26,17 +27,8 @@ export default async function(
});
}
//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (mutedUserIds.indexOf(note.userId) != -1) {
return;
}
if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
return;
}
if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
return;
}
//#endregion
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, mutedUserIds)) return;
connection.send(JSON.stringify({
type: 'note',

View File

@@ -4,6 +4,7 @@ import Xev from 'xev';
import { IUser } from '../../../models/user';
import Mute from '../../../models/mute';
import { pack } from '../../../models/note';
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
export default async function(
request: websocket.request,
@@ -23,17 +24,8 @@ export default async function(
});
}
//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (mutedUserIds.indexOf(note.userId) != -1) {
return;
}
if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
return;
}
if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
return;
}
//#endregion
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, mutedUserIds)) return;
connection.send(JSON.stringify({
type: 'note',

View File

@@ -14,6 +14,7 @@ import reversiGameStream from './stream/games/reversi-game';
import reversiStream from './stream/games/reversi';
import serverStatsStream from './stream/server-stats';
import notesStatsStream from './stream/notes-stats';
import hashtagStream from './stream/hashtag';
import { ParsedUrlQuery } from 'querystring';
import authenticate from './authenticate';
@@ -44,6 +45,12 @@ module.exports = (server: http.Server) => {
ev.removeAllListeners();
});
connection.on('message', async (data) => {
if (data.utf8Data == 'ping') {
connection.send('pong');
}
});
const q = request.resourceURL.query as ParsedUrlQuery;
const [user, app] = await authenticate(q.i as string);
@@ -57,6 +64,11 @@ module.exports = (server: http.Server) => {
return;
}
if (request.resourceURL.pathname === '/hashtag') {
hashtagStream(request, connection, ev, user);
return;
}
if (user == null) {
connection.send('authentication-failed');
connection.close();

61
src/services/i/pin.ts Normal file
View File

@@ -0,0 +1,61 @@
import config from '../../config';
import * as mongo from 'mongodb';
import User, { isLocalUser, isRemoteUser, ILocalUser } from '../../models/user';
import Following from '../../models/following';
import renderAdd from '../../remote/activitypub/renderer/add';
import renderRemove from '../../remote/activitypub/renderer/remove';
import packAp from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
export async function deliverPinnedChange(userId: mongo.ObjectID, oldId?: mongo.ObjectID, newId?: mongo.ObjectID) {
const user = await User.findOne({
_id: userId
});
if (!isLocalUser(user)) return;
const queue = await CreateRemoteInboxes(user);
if (queue.length < 1) return;
const target = `${config.url}/users/${user._id}/collections/featured`;
if (oldId) {
const oldItem = `${config.url}/notes/${oldId}`;
const content = packAp(renderRemove(user, target, oldItem));
queue.forEach(inbox => {
deliver(user, content, inbox);
});
}
if (newId) {
const newItem = `${config.url}/notes/${newId}`;
const content = packAp(renderAdd(user, target, newItem));
queue.forEach(inbox => {
deliver(user, content, inbox);
});
}
}
/**
* ローカルユーザーのリモートフォロワーのinboxリストを作成する
* @param user ローカルユーザー
*/
async function CreateRemoteInboxes(user: ILocalUser): Promise<string[]> {
const followers = await Following.find({
followeeId: user._id
});
const queue: string[] = [];
followers.map(following => {
const follower = following._follower;
if (isRemoteUser(follower)) {
const inbox = follower.sharedInbox || follower.inbox;
if (!queue.includes(inbox)) queue.push(inbox);
}
});
return queue;
}

View File

@@ -1,7 +1,7 @@
import es from '../../db/elasticsearch';
import Note, { pack, INote } from '../../models/note';
import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user';
import { publishUserStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../stream';
import { publishUserStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream, publishHashtagStream } from '../../stream';
import Following from '../../models/following';
import { deliver } from '../../queue';
import renderNote from '../../remote/activitypub/renderer/note';
@@ -138,6 +138,18 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
const mentionedUsers = await extractMentionedUsers(tokens);
if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) {
mentionedUsers.push(await User.findOne({ _id: data.reply.userId }));
}
if (data.visibility == 'specified') {
data.visibleUsers.forEach(u => {
if (!mentionedUsers.some(x => x._id.equals(u._id))) {
mentionedUsers.push(u);
}
});
}
const note = await insertNote(user, data, tags, mentionedUsers);
res(note);
@@ -177,10 +189,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
noteObj.isFirstNote = true;
}
if (tags.length > 0) {
publishHashtagStream(noteObj);
}
const nm = new NotificationManager(user, note);
const nmRelatedPromises = [];
createMentionedEvents(mentionedUsers, noteObj, nm);
createMentionedEvents(mentionedUsers, note, nm);
const noteActivity = await renderActivity(data, note);
@@ -310,7 +326,7 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren
if (['public', 'home', 'followers'].includes(note.visibility)) {
// フォロワーに配信
publishToFollowers(note, noteObj, user, noteActivity);
publishToFollowers(note, user, noteActivity);
}
// リストに配信
@@ -448,7 +464,7 @@ async function publishToUserLists(note: INote, noteObj: any) {
});
}
async function publishToFollowers(note: INote, noteObj: any, user: IUser, noteActivity: any) {
async function publishToFollowers(note: INote, user: IUser, noteActivity: any) {
const detailPackedNote = await pack(note, null, {
detail: true,
skipHide: true
@@ -497,9 +513,13 @@ function deliverNoteToMentionedRemoteUsers(mentionedUsers: IUser[], user: ILocal
});
}
function createMentionedEvents(mentionedUsers: IUser[], noteObj: any, nm: NotificationManager) {
function createMentionedEvents(mentionedUsers: IUser[], note: INote, nm: NotificationManager) {
mentionedUsers.filter(u => isLocalUser(u)).forEach(async (u) => {
publishUserStream(u._id, 'mention', noteObj);
const detailPackedNote = await pack(note, u, {
detail: true
});
publishUserStream(u._id, 'mention', detailPackedNote);
// Create notification
nm.push(u._id, 'mention');

View File

@@ -78,6 +78,10 @@ class Publisher {
public publishGlobalTimelineStream = (note: any): void => {
this.publish('global-timeline', null, note);
}
public publishHashtagStream = (note: any): void => {
this.publish('hashtag', null, note);
}
}
const publisher = new Publisher();
@@ -95,3 +99,4 @@ export const publishReversiGameStream = publisher.publishReversiGameStream;
export const publishLocalTimelineStream = publisher.publishLocalTimelineStream;
export const publishHybridTimelineStream = publisher.publishHybridTimelineStream;
export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
export const publishHashtagStream = publisher.publishHashtagStream;

View File

@@ -1,6 +1,7 @@
import * as assert from 'assert';
import analyze from '../src/mfm/parse';
import toHtml from '../src/mfm/html';
import syntaxhighlighter from '../src/mfm/parse/core/syntax-highlighter';
describe('Text', () => {
@@ -70,11 +71,20 @@ describe('Text', () => {
});
it('hashtag', () => {
const tokens = analyze('Strawberry Pasta #alice');
const tokens1 = analyze('Strawberry Pasta #alice');
assert.deepEqual([
{ type: 'text', content: 'Strawberry Pasta ' },
{ type: 'hashtag', content: '#alice', hashtag: 'alice' }
], tokens);
], tokens1);
const tokens2 = analyze('Foo #bar, baz #piyo.');
assert.deepEqual([
{ type: 'text', content: 'Foo ' },
{ type: 'hashtag', content: '#bar', hashtag: 'bar' },
{ type: 'text', content: ', baz ' },
{ type: 'hashtag', content: '#piyo', hashtag: 'piyo' },
{ type: 'text', content: '.' }
], tokens2);
});
it('url', () => {
@@ -170,4 +180,12 @@ describe('Text', () => {
assert.equal(html, '<span class="symbol">/</span>');
});
});
describe('toHtml', () => {
it('br', () => {
const input = 'foo\nbar\nbaz';
const output = '<p>foo<br>bar<br>baz</p>';
assert.equal(toHtml(analyze(input)), output);
});
});
});