Compare commits
72 Commits
Author | SHA1 | Date | |
---|---|---|---|
ccfd48232a | |||
429bf179dc | |||
8ba3fb13eb | |||
11496d887e | |||
bec48319ec | |||
71a93b2b43 | |||
6ed3f9e414 | |||
dc8f592c1f | |||
f66c31c771 | |||
55e2ae1408 | |||
19c72627fc | |||
2a4c53c3a4 | |||
1f2ebce8ed | |||
fcea9dacb7 | |||
908872f374 | |||
f688ceafb8 | |||
b47b5d6d8b | |||
31ce3aa312 | |||
5b22d92e99 | |||
df148e25da | |||
4b26df5c3a | |||
e765be4205 | |||
f7d2457063 | |||
6032d803aa | |||
0de371db38 | |||
ce3797c4af | |||
56dd8c298b | |||
3533257efe | |||
dc2f08721d | |||
66608a4131 | |||
2fa90131eb | |||
a51ed28db6 | |||
5ec290663b | |||
1374d6e34d | |||
45ade17c58 | |||
c753e26187 | |||
577929eed1 | |||
1fde8a8fb0 | |||
77e53cbf9e | |||
ab83e08bc7 | |||
2fad6e6d5f | |||
a3604a6c95 | |||
44f6fe6f1f | |||
311b4e90ca | |||
f5a937c523 | |||
0632a3ed3f | |||
71bada97df | |||
62509edcbe | |||
f97cdfaa20 | |||
67ec10e86d | |||
481b3f2c58 | |||
7d599a68ea | |||
7ccff732b8 | |||
7587c896d5 | |||
91297f1ab3 | |||
d872a16fe0 | |||
60aa35adf8 | |||
5035b66773 | |||
fa9da8ecab | |||
1f9bca7188 | |||
ffa5bdeb50 | |||
e6bfb7398e | |||
6def0c776f | |||
24bae9eaed | |||
fb5175a283 | |||
6e49437154 | |||
2511ed56ac | |||
c4bfc99cf5 | |||
4efe38440d | |||
4a5f2c3c40 | |||
109738ccb9 | |||
25438c4d64 |
@ -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')
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -112,7 +112,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
reduce-motion: "Réduire les animations dans l’interface utilisateur"
|
||||
this-setting-is-this-device-only: "Uniquement sur cet appareil"
|
||||
do-not-use-in-production: 'Il s’agit d’une 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 d’abonnement en attente d’approbation"
|
||||
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 d’abonnement en attente d’approbation"
|
||||
advanced: "Avancé"
|
||||
privacy: "Vie privée"
|
||||
save: "Mettre à jour le profil"
|
||||
|
@ -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:
|
||||
|
@ -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: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "8.44.1",
|
||||
"clientVersion": "1.0.9813",
|
||||
"version": "8.51.0",
|
||||
"clientVersion": "1.0.9887",
|
||||
"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",
|
||||
|
106
src/client/app/common/hotkey.ts
Normal file
106
src/client/app/common/hotkey.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
33
src/client/app/common/keycode.ts
Normal file
33
src/client/app/common/keycode.ts
Normal 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}`];
|
||||
}
|
13
src/client/app/common/scripts/streaming/hashtag.ts
Normal file
13
src/client/app/common/scripts/streaming/hashtag.ts
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -237,6 +237,10 @@ export default Vue.extend({
|
||||
|
||||
warp(date) {
|
||||
(this.$refs.tl as any).warp(date);
|
||||
},
|
||||
|
||||
focus() {
|
||||
(this.$refs.tl as any).focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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();
|
||||
|
65
src/client/app/desktop/views/components/settings.tags.vue
Normal file
65
src/client/app/desktop/views/components/settings.tags.vue
Normal 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>
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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">
|
||||
|
@ -190,8 +190,8 @@ export default Vue.extend({
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.destroyDom();
|
||||
this.$emit('closed');
|
||||
this.destroyDom();
|
||||
}, 300);
|
||||
},
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
97
src/client/app/desktop/views/pages/deck/deck.direct.vue
Normal file
97
src/client/app/desktop/views/pages/deck/deck.direct.vue
Normal 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>
|
117
src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
Normal file
117
src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
Normal 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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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%',
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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';
|
||||
|
@ -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');
|
||||
|
@ -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();
|
||||
},
|
||||
|
@ -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" 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>
|
||||
@ -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
|
||||
|
@ -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>
|
||||
|
@ -10,6 +10,7 @@ const defaultSettings = {
|
||||
home: null,
|
||||
mobileHome: [],
|
||||
deck: null,
|
||||
tagTimelines: [],
|
||||
fetchOnScroll: true,
|
||||
showMaps: true,
|
||||
showPostFormOnTopOfTl: false,
|
||||
|
@ -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"
|
||||
|
@ -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]
|
||||
|
15
src/misc/should-mute-this-note.ts
Normal file
15
src/misc/should-mute-this-note.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
|
@ -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)) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
9
src/remote/activitypub/renderer/add.ts
Normal file
9
src/remote/activitypub/renderer/add.ts
Normal 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
|
||||
});
|
@ -82,20 +82,38 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
|
||||
|
||||
const files = await promisedFiles;
|
||||
|
||||
const text = note.text ? parseMfm(note.text).map(x => {
|
||||
if (x.type == 'mention' && x.host == null) {
|
||||
return `${x.content}@${config.host}`;
|
||||
} else {
|
||||
return x.content;
|
||||
}
|
||||
}).join('') : null;
|
||||
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),
|
||||
content: toHtml(Object.assign({}, note, { text })),
|
||||
_misskey_content: text,
|
||||
published: note.createdAt.toISOString(),
|
||||
to,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
9
src/remote/activitypub/renderer/remove.ts
Normal file
9
src/remote/activitypub/renderer/remove.ts
Normal 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
|
||||
});
|
@ -53,6 +53,7 @@ export interface IPerson extends IObject {
|
||||
publicKey: any;
|
||||
followers: any;
|
||||
following: any;
|
||||
featured?: any;
|
||||
outbox: any;
|
||||
endpoints: string[];
|
||||
}
|
||||
|
@ -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);
|
||||
|
38
src/server/activitypub/featured.ts
Normal file
38
src/server/activitypub/featured.ts
Normal 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);
|
||||
};
|
@ -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);
|
||||
});
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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',
|
||||
|
40
src/server/api/stream/hashtag.ts
Normal file
40
src/server/api/stream/hashtag.ts
Normal 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
|
||||
}));
|
||||
});
|
||||
}
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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
61
src/services/i/pin.ts
Normal 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;
|
||||
}
|
@ -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');
|
||||
|
@ -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;
|
||||
|
13
test/mfm.ts
13
test/mfm.ts
@ -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', () => {
|
||||
|
Reference in New Issue
Block a user