Compare commits

..

60 Commits

Author SHA1 Message Date
908872f374 8.47.0 2018-09-18 05:36:14 +09:00
f688ceafb8 Merge pull request #2729 from syuilo/greenkeeper/@types/node-10.10.1
Update @types/node to the latest version 🚀
2018-09-18 05:35:49 +09:00
b47b5d6d8b Merge pull request #2728 from syuilo/l10n_develop
New Crowdin translations
2018-09-18 05:35:39 +09:00
31ce3aa312 キーボードショートカットを強化するなど 2018-09-18 05:35:06 +09:00
5b22d92e99 New translations ja-JP.yml (English) 2018-09-18 03:51:42 +09:00
df148e25da fix(package): update @types/node to version 10.10.1 2018-09-17 17:24:15 +00:00
4b26df5c3a New translations ja-JP.yml (English) 2018-09-18 02:19:32 +09:00
e765be4205 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-09-18 02:15:36 +09:00
f7d2457063 New translations ja-JP.yml (Norwegian) 2018-09-18 02:15:27 +09:00
6032d803aa New translations ja-JP.yml (Dutch) 2018-09-18 02:15:25 +09:00
0de371db38 New translations ja-JP.yml (Japanese, Kansai) 2018-09-18 02:15:22 +09:00
ce3797c4af 8.46.0 2018-09-18 02:15:19 +09:00
56dd8c298b New translations ja-JP.yml (Spanish) 2018-09-18 02:15:19 +09:00
3533257efe New translations ja-JP.yml (Russian) 2018-09-18 02:15:16 +09:00
dc2f08721d New translations ja-JP.yml (Portuguese) 2018-09-18 02:15:14 +09:00
66608a4131 New translations ja-JP.yml (Polish) 2018-09-18 02:15:11 +09:00
2fa90131eb New translations ja-JP.yml (Korean) 2018-09-18 02:15:08 +09:00
a51ed28db6 New translations ja-JP.yml (Italian) 2018-09-18 02:15:06 +09:00
5ec290663b New translations ja-JP.yml (German) 2018-09-18 02:15:03 +09:00
1374d6e34d New translations ja-JP.yml (French) 2018-09-18 02:15:00 +09:00
45ade17c58 New translations ja-JP.yml (English) 2018-09-18 02:14:57 +09:00
c753e26187 New translations ja-JP.yml (Chinese Simplified) 2018-09-18 02:14:54 +09:00
577929eed1 New translations ja-JP.yml (Catalan) 2018-09-18 02:14:51 +09:00
1fde8a8fb0 Merge pull request #2727 from syuilo/greenkeeper/@types/node-10.10.0
Update @types/node to the latest version 🚀
2018-09-18 02:14:32 +09:00
77e53cbf9e Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-09-18 02:14:16 +09:00
ab83e08bc7 メッセージタイムラインを追加 2018-09-18 02:14:12 +09:00
2fad6e6d5f Refactor 2018-09-18 02:13:42 +09:00
a3604a6c95 Merge pull request #2722 from syuilo/l10n_develop
New Crowdin translations
2018-09-17 23:12:54 +09:00
44f6fe6f1f Refactor: Extract shouldMuteThisNote function 2018-09-17 23:07:15 +09:00
311b4e90ca No lint when test 2018-09-17 22:51:25 +09:00
f5a937c523 Better hashtag parsing 2018-09-17 22:51:10 +09:00
0632a3ed3f fix(package): update @types/node to version 10.10.0 2018-09-17 08:10:08 +00:00
71bada97df 8.45.1 2018-09-17 12:19:54 +09:00
62509edcbe Refactor 2018-09-17 12:18:59 +09:00
f97cdfaa20 Fix #2725 2018-09-17 11:59:24 +09:00
67ec10e86d Add untilId param 2018-09-17 11:43:53 +09:00
481b3f2c58 New translations ja-JP.yml (English) 2018-09-17 09:11:27 +09:00
7d599a68ea pong 2018-09-17 09:07:46 +09:00
7ccff732b8 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-09-17 09:07:05 +09:00
7587c896d5 8.45.0 2018-09-17 09:06:56 +09:00
91297f1ab3 Merge pull request #2717 from syuilo/l10n_develop
New Crowdin translations
2018-09-17 09:06:25 +09:00
d872a16fe0 🎨 2018-09-17 09:05:51 +09:00
60aa35adf8 New translations ja-JP.yml (Norwegian) 2018-09-17 09:01:59 +09:00
5035b66773 New translations ja-JP.yml (Dutch) 2018-09-17 09:01:57 +09:00
fa9da8ecab New translations ja-JP.yml (Japanese, Kansai) 2018-09-17 09:01:54 +09:00
1f9bca7188 New translations ja-JP.yml (Spanish) 2018-09-17 09:01:52 +09:00
ffa5bdeb50 New translations ja-JP.yml (Russian) 2018-09-17 09:01:49 +09:00
e6bfb7398e New translations ja-JP.yml (Portuguese) 2018-09-17 09:01:47 +09:00
6def0c776f New translations ja-JP.yml (Polish) 2018-09-17 09:01:45 +09:00
24bae9eaed New translations ja-JP.yml (Korean) 2018-09-17 09:01:43 +09:00
fb5175a283 New translations ja-JP.yml (Italian) 2018-09-17 09:01:40 +09:00
6e49437154 New translations ja-JP.yml (German) 2018-09-17 09:01:38 +09:00
2511ed56ac New translations ja-JP.yml (French) 2018-09-17 09:01:35 +09:00
c4bfc99cf5 New translations ja-JP.yml (English) 2018-09-17 09:01:33 +09:00
4efe38440d New translations ja-JP.yml (Chinese Simplified) 2018-09-17 09:01:30 +09:00
4a5f2c3c40 New translations ja-JP.yml (Catalan) 2018-09-17 09:01:27 +09:00
109738ccb9 ハッシュタグタイムラインを実装 2018-09-17 09:00:20 +09:00
433dbe179d 8.44.1 2018-09-17 03:37:22 +09:00
b21b33831a Fix bug 2018-09-17 03:36:58 +09:00
25438c4d64 New translations ja-JP.yml (French) 2018-09-17 00:01:10 +09:00
72 changed files with 1450 additions and 357 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

@ -155,8 +155,10 @@ common:
home: "ホーム"
local: "ローカル"
hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知"
list: "リスト"
swap-left: "左に移動"
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、"
adjective: "さん"
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue:

View File

@ -155,8 +155,10 @@ common:
home: "Startseite"
local: "Lokal"
hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "Global"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "Mitteilungen"
list: "Listen"
swap-left: "Nach links"
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
hybrid: "ソーシャル"
global: "Global"
mentions: "あなた宛て"
messages: "メッセージ"
list: "Listen"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、"
adjective: "さん"
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue:

View File

@ -110,7 +110,7 @@ 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"
reduce-motion: "Reduce motion in UI"
this-setting-is-this-device-only: "Only for this device"
@ -155,8 +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"
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
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"
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
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

@ -155,8 +155,10 @@ common:
home: "Inicio"
local: "Local"
hybrid: "Social"
hashtag: "ハッシュタグ"
global: "Global"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "Notificaciones"
list: "Listado"
swap-left: "Desplazar a la izq."
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
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"
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
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: "ユーザー名のホストを省略しない"
reduce-motion: "UIの動きを減らす"
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,8 +155,10 @@ common:
home: "Accueil"
local: "Local"
hybrid: "Social"
hashtag: "ハッシュタグ"
global: "Global"
mentions: "あなた宛て"
mentions: "Mentions"
direct: "ダイレクト投稿"
notifications: "Notifications"
list: "Liste"
swap-left: "Déplacer à gauche"
@ -260,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"
@ -484,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"
@ -791,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"
@ -808,8 +810,13 @@ desktop/views/components/timeline.vue:
local: "Local"
hybrid: "Social"
global: "Global"
mentions: "あなた宛て"
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: "さん"
@ -1134,7 +1141,8 @@ mobile/views/pages/home.vue:
local: "Local"
hybrid: "Social"
global: "Global"
mentions: "あなた宛て"
mentions: "Mentions"
messages: "メッセージ"
mobile/views/pages/tag.vue:
no-posts-found: "Pas de message avec un hashtag {} trouvé."
mobile/views/pages/welcome.vue:
@ -1175,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

@ -155,8 +155,10 @@ common:
home: "ホーム"
local: "ローカル"
hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知"
list: "リスト"
swap-left: "左に移動"
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、"
adjective: "さん"
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue:

View File

@ -166,8 +166,10 @@ common:
home: "ホーム"
local: "ローカル"
hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知"
list: "リスト"
swap-left: "左に移動"
@ -915,7 +917,12 @@ desktop/views/components/timeline.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、"
@ -1317,6 +1324,7 @@ mobile/views/pages/home.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"

View File

@ -155,8 +155,10 @@ common:
home: "うち"
local: "ローカル"
hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知"
list: "リスト"
swap-left: "左に移動や!"
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue:
welcome-back: "おかえり、"
adjective: "さん"
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿はあらへんで。"
mobile/views/pages/welcome.vue:

View File

@ -155,8 +155,10 @@ common:
home: "홈"
local: "로컬"
hybrid: "소셜"
hashtag: "ハッシュタグ"
global: "글로벌"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "통지"
list: "목록"
swap-left: "左に移動"
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、"
adjective: "さん"
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue:

View File

@ -155,8 +155,10 @@ common:
home: "ホーム"
local: "ローカル"
hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知"
list: "リスト"
swap-left: "左に移動"
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
hybrid: "ソーシャル"
global: "Algemeen"
mentions: "あなた宛て"
messages: "メッセージ"
list: "Lijsten"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、"
adjective: "さん"
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue:

View File

@ -155,8 +155,10 @@ common:
home: "ホーム"
local: "ローカル"
hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知"
list: "リスト"
swap-left: "左に移動"
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、"
adjective: "さん"
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue:

View File

@ -155,8 +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"
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
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: "さん"
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
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

@ -155,8 +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"
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、"
adjective: "さん"
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue:

View File

@ -155,8 +155,10 @@ common:
home: "ホーム"
local: "ローカル"
hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知"
list: "リスト"
swap-left: "左に移動"
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、"
adjective: "さん"
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
mobile/views/pages/welcome.vue:

View File

@ -155,8 +155,10 @@ common:
home: "ホーム"
local: "ローカル"
hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知"
list: "リスト"
swap-left: "左に移動"
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、"
adjective: "さん"
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
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.44.0",
"clientVersion": "1.0.9813",
"version": "8.47.0",
"clientVersion": "1.0.9873",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,
@ -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,79 @@
import keyCode from './keycode';
const getKeyMap = keymap => Object.keys(keymap).map(input => {
const result = {} as any;
const { keyup, keydown } = keymap[input];
input.split('+').forEach(keyName => {
switch (keyName.toLowerCase()) {
case 'ctrl':
case 'alt':
case 'shift':
case 'meta':
result[keyName] = true;
break;
default:
result.keyCode = keyCode(keyName);
}
});
result.callback = {
keydown: keydown || keymap[input],
keyup
};
return result;
});
const ignoreElemens = ['input', 'textarea'];
export default {
install(Vue) {
Vue.directive('hotkey', {
bind(el, binding) {
el._hotkey_global = binding.modifiers.global === true;
el._keymap = getKeyMap(binding.value);
el.dataset.reservedKeyCodes = el._keymap.map(key => `'${key.keyCode}'`).join(' ');
el._keyHandler = e => {
const reservedKeyCodes = document.activeElement ? ((document.activeElement as any).dataset || {}).reservedKeyCodes || '' : '';
if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
for (const hotkey of el._keymap) {
if (el._hotkey_global && reservedKeyCodes.includes(`'${e.keyCode}'`)) break;
const callback = hotkey.keyCode === e.keyCode &&
!!hotkey.ctrl === e.ctrlKey &&
!!hotkey.alt === e.altKey &&
!!hotkey.shift === e.shiftKey &&
!!hotkey.meta === e.metaKey &&
hotkey.callback[e.type];
if (callback) {
e.preventDefault();
e.stopPropagation();
callback(e);
}
}
};
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,139 @@
export default searchInput => {
// Keyboard Events
if (searchInput && typeof searchInput === 'object') {
const hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode;
if (hasKeyCode) {
searchInput = hasKeyCode;
}
}
// Numbers
// if (typeof searchInput === 'number') {
// return names[searchInput]
// }
// Everything else (cast to string)
const search = String(searchInput);
// check codes
const foundNamedKeyCodes = codes[search.toLowerCase()];
if (foundNamedKeyCodes) {
return foundNamedKeyCodes;
}
// check aliases
const foundNamedKeyAliases = aliases[search.toLowerCase()];
if (foundNamedKeyAliases) {
return foundNamedKeyAliases;
}
// weird character?
if (search.length === 1) {
return search.charCodeAt(0);
}
return undefined;
};
/**
* Get by name
*
* exports.code['enter'] // => 13
*/
export const codes = {
'backspace': 8,
'tab': 9,
'enter': 13,
'shift': 16,
'ctrl': 17,
'alt': 18,
'pause/break': 19,
'caps lock': 20,
'esc': 27,
'space': 32,
'page up': 33,
'page down': 34,
'end': 35,
'home': 36,
'left': 37,
'up': 38,
'right': 39,
'down': 40,
// 'add': 43,
'insert': 45,
'delete': 46,
'command': 91,
'left command': 91,
'right command': 93,
'numpad *': 106,
// 'numpad +': 107,
'numpad +': 43,
'numpad add': 43, // as a trick
'numpad -': 109,
'numpad .': 110,
'numpad /': 111,
'num lock': 144,
'scroll lock': 145,
'my computer': 182,
'my calculator': 183,
';': 186,
'=': 187,
',': 188,
'-': 189,
'.': 190,
'/': 191,
'`': 192,
'[': 219,
'\\': 220,
']': 221,
"'": 222
};
// Helper aliases
export const aliases = {
'windows': 91,
'⇧': 16,
'⌥': 18,
'⌃': 17,
'⌘': 91,
'ctl': 17,
'control': 17,
'option': 18,
'pause': 19,
'break': 19,
'caps': 20,
'return': 13,
'escape': 27,
'spc': 32,
'pgup': 33,
'pgdn': 34,
'ins': 45,
'del': 46,
'cmd': 91
};
/*!
* Programatically add the following
*/
// lower case chars
for (let i = 97; i < 123; i++) {
codes[String.fromCharCode(i)] = i - 32;
}
// numbers
for (let i = 48; i < 58; i++) {
codes[i - 48] = i;
}
// function keys
for (let i = 1; i < 13; i++) {
codes['f' + i] = i + 111;
}
// numpad keys
for (let i = 0; i < 10; i++) {
codes['numpad ' + i] = i + 96;
}

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,5 +1,5 @@
<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>
@ -31,28 +31,51 @@ 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
}
},
data() {
return {
title: placeholder
};
},
computed: {
keymap(): any {
return {
'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'),
};
}
},
mounted() {
this.$nextTick(() => {
const popover = this.$refs.popover as any;
@ -88,6 +111,7 @@ export default Vue.extend({
});
});
},
methods: {
react(reaction) {
(this as any).api('notes/reactions/create', {
@ -95,15 +119,19 @@ 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({
@ -120,7 +148,10 @@ export default Vue.extend({
scale: 0.5,
duration: 200,
easing: 'easeInBack',
complete: () => this.destroyDom()
complete: () => {
this.$emit('closed');
this.destroyDom();
}
});
}
}

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>
@ -111,6 +111,18 @@ export default Vue.extend({
},
computed: {
keymap(): any {
return {
'r': this.reply,
'a': this.react,
'n': this.renote,
'up': this.focusBefore,
'shift+tab': this.focusBefore,
'down': this.focusAfter,
'tab': this.focusAfter,
};
},
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
@ -223,64 +235,39 @@ 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() {
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.p
});
}).$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;
focusBefore() {
focus(this.$el, e => e.previousElementSibling);
},
case e.which == 40: // [↓]
case e.which == 75: // [k]
case e.which == 9: // [Tab]
focus(this.$el, e => e.nextElementSibling);
break;
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

@ -12,7 +12,7 @@
<!-- トランジションを有効にするとなぜかメモリリークする -->
<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

@ -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>
@ -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,

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,55 +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;
case 'mentions': return (this as any).os.stream;
}
},
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';
case 'mentions': return 'notes/mentions';
}
},
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(this.src == 'mentions' ? 'mention' : '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(this.src == 'mentions' ? 'mention' : '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: {
@ -98,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;
@ -121,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) {
@ -142,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();
},
@ -158,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,15 +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 == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</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>
@ -21,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: {
@ -32,6 +40,7 @@ export default Vue.extend({
return {
src: 'home',
list: null,
tagTl: null,
enableLocalTimeline: false
};
},
@ -41,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;
}
},
@ -55,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';
@ -71,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
});
}
}
@ -106,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,7 +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">
@ -15,13 +17,15 @@ 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,
XMentionsColumn
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

@ -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

@ -147,6 +147,15 @@ export default Vue.extend({
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%',
@ -161,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

@ -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

@ -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,51 +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;
case 'mentions': return (this as any).os.stream;
}
},
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';
case 'mentions': return 'notes/mentions';
}
},
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(this.src == 'mentions' ? 'mention' : '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(this.src == 'mentions' ? 'mention' : '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: {
@ -93,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;
@ -116,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) {
@ -137,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

@ -7,7 +7,9 @@
<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>
@ -22,16 +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"></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>
@ -42,6 +50,8 @@
<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>
@ -63,6 +73,7 @@ export default Vue.extend({
src: 'home',
list: null,
lists: null,
tagTl: null,
showNav: false,
enableLocalTimeline: false
};
@ -74,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) {
@ -97,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';
@ -121,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
});
},
@ -137,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
@ -153,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

@ -10,6 +10,7 @@ const defaultSettings = {
home: null,
mobileHome: [],
deck: null,
tagTimelines: [],
fetchOnScroll: true,
showMaps: true,
showPostFormOnTopOfTl: false,

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

@ -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

@ -196,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

@ -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

@ -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

@ -27,6 +27,9 @@ export const meta = {
untilId: $.type(ID).optional.note({
}),
visibility: $.str.optional.note({
}),
}
};
@ -52,6 +55,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
_id: -1
};
if (ps.visibility) {
query.visibility = ps.visibility;
}
if (ps.following) {
const followingIds = await getFriendIds(user._id);

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();

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';
@ -142,6 +142,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
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);
@ -181,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);
@ -314,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);
}
// リストに配信
@ -452,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
@ -501,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

@ -71,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', () => {