Compare commits
60 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
433dbe179d | |||
b21b33831a | |||
25438c4d64 |
@ -78,7 +78,7 @@ gulp.task('build:copy', ['build:copy:views', 'build:copy:lang'], () =>
|
|||||||
]).pipe(gulp.dest('./built/'))
|
]).pipe(gulp.dest('./built/'))
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task('test', ['lint', 'mocha']);
|
gulp.task('test', ['mocha']);
|
||||||
|
|
||||||
gulp.task('lint', () =>
|
gulp.task('lint', () =>
|
||||||
gulp.src('./src/**/*.ts')
|
gulp.src('./src/**/*.ts')
|
||||||
|
@ -155,8 +155,10 @@ common:
|
|||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
local: "ローカル"
|
local: "ローカル"
|
||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
swap-left: "左に移動"
|
swap-left: "左に移動"
|
||||||
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "おかえりなさい、"
|
welcome-back: "おかえりなさい、"
|
||||||
adjective: "さん"
|
adjective: "さん"
|
||||||
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
|
@ -155,8 +155,10 @@ common:
|
|||||||
home: "Startseite"
|
home: "Startseite"
|
||||||
local: "Lokal"
|
local: "Lokal"
|
||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "Global"
|
global: "Global"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "Mitteilungen"
|
notifications: "Mitteilungen"
|
||||||
list: "Listen"
|
list: "Listen"
|
||||||
swap-left: "Nach links"
|
swap-left: "Nach links"
|
||||||
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "Global"
|
global: "Global"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "Listen"
|
list: "Listen"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "おかえりなさい、"
|
welcome-back: "おかえりなさい、"
|
||||||
adjective: "さん"
|
adjective: "さん"
|
||||||
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
|
@ -110,7 +110,7 @@ common:
|
|||||||
verified-user: "Verified account"
|
verified-user: "Verified account"
|
||||||
disable-animated-mfm: "Disable animated texts in a post"
|
disable-animated-mfm: "Disable animated texts in a post"
|
||||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
always-mark-nsfw: "Always post with a warning about media attachment"
|
||||||
show-full-acct: "Do not omit the hostname from the username"
|
show-full-acct: "Do not omit the hostname from the username"
|
||||||
reduce-motion: "Reduce motion in UI"
|
reduce-motion: "Reduce motion in UI"
|
||||||
this-setting-is-this-device-only: "Only for this device"
|
this-setting-is-this-device-only: "Only for this device"
|
||||||
@ -155,8 +155,10 @@ common:
|
|||||||
home: "Home"
|
home: "Home"
|
||||||
local: "Local"
|
local: "Local"
|
||||||
hybrid: "Social"
|
hybrid: "Social"
|
||||||
|
hashtag: "Hashtag"
|
||||||
global: "Global"
|
global: "Global"
|
||||||
mentions: "Mentions"
|
mentions: "Mentions"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "Notifications"
|
notifications: "Notifications"
|
||||||
list: "Lists"
|
list: "Lists"
|
||||||
swap-left: "Move to the left"
|
swap-left: "Move to the left"
|
||||||
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "Social"
|
hybrid: "Social"
|
||||||
global: "Global"
|
global: "Global"
|
||||||
mentions: "Mentions"
|
mentions: "Mentions"
|
||||||
|
messages: "Messages"
|
||||||
list: "Lists"
|
list: "Lists"
|
||||||
|
hashtag: "Hashtag"
|
||||||
|
add-tag-timeline: "Add hashtag tl"
|
||||||
|
add-list: "Add list"
|
||||||
|
list-name: "List name"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "Welcome back,"
|
welcome-back: "Welcome back,"
|
||||||
adjective: "-san"
|
adjective: "-san"
|
||||||
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "Social"
|
hybrid: "Social"
|
||||||
global: "Global"
|
global: "Global"
|
||||||
mentions: "Mentions"
|
mentions: "Mentions"
|
||||||
|
messages: "Messages"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "No posts \"{}\" found."
|
no-posts-found: "No posts \"{}\" found."
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
|
@ -155,8 +155,10 @@ common:
|
|||||||
home: "Inicio"
|
home: "Inicio"
|
||||||
local: "Local"
|
local: "Local"
|
||||||
hybrid: "Social"
|
hybrid: "Social"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "Global"
|
global: "Global"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "Notificaciones"
|
notifications: "Notificaciones"
|
||||||
list: "Listado"
|
list: "Listado"
|
||||||
swap-left: "Desplazar a la izq."
|
swap-left: "Desplazar a la izq."
|
||||||
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "Bienvenido/a de vuelta,"
|
welcome-back: "Bienvenido/a de vuelta,"
|
||||||
adjective: "-san"
|
adjective: "-san"
|
||||||
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
|
@ -112,7 +112,7 @@ common:
|
|||||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||||
show-full-acct: "ユーザー名のホストを省略しない"
|
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"
|
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.'
|
do-not-use-in-production: 'Il s’agit d’une version de développement. Ne pas utiliser dans un environnement de production.'
|
||||||
reversi:
|
reversi:
|
||||||
@ -155,8 +155,10 @@ common:
|
|||||||
home: "Accueil"
|
home: "Accueil"
|
||||||
local: "Local"
|
local: "Local"
|
||||||
hybrid: "Social"
|
hybrid: "Social"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "Global"
|
global: "Global"
|
||||||
mentions: "あなた宛て"
|
mentions: "Mentions"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "Notifications"
|
notifications: "Notifications"
|
||||||
list: "Liste"
|
list: "Liste"
|
||||||
swap-left: "Déplacer à gauche"
|
swap-left: "Déplacer à gauche"
|
||||||
@ -260,8 +262,8 @@ common/views/components/connect-failed.troubleshooter.vue:
|
|||||||
flush: "Vider le cache"
|
flush: "Vider le cache"
|
||||||
set-version: "Choisissez une version"
|
set-version: "Choisissez une version"
|
||||||
common/views/components/media-banner.vue:
|
common/views/components/media-banner.vue:
|
||||||
sensitive: "閲覧注意"
|
sensitive: "Contenu sensible"
|
||||||
click-to-show: "クリックして表示"
|
click-to-show: "Cliquer pour afficher"
|
||||||
common/views/components/cw-button.vue:
|
common/views/components/cw-button.vue:
|
||||||
hide: "Masquer"
|
hide: "Masquer"
|
||||||
show: "Voir plus"
|
show: "Voir plus"
|
||||||
@ -484,7 +486,7 @@ desktop/views/components/charts.vue:
|
|||||||
drive-files-total: "ドライブのファイル数の累計"
|
drive-files-total: "ドライブのファイル数の累計"
|
||||||
network-requests: "Requêtes"
|
network-requests: "Requêtes"
|
||||||
network-time: "Temps de réponse"
|
network-time: "Temps de réponse"
|
||||||
network-usage: "通信量"
|
network-usage: "Traffic"
|
||||||
desktop/views/components/choose-file-from-drive-window.vue:
|
desktop/views/components/choose-file-from-drive-window.vue:
|
||||||
choose-file: "Sélection de fichiers"
|
choose-file: "Sélection de fichiers"
|
||||||
upload: "Téléverser des fichiers à partir de votre ordinateur"
|
upload: "Téléverser des fichiers à partir de votre ordinateur"
|
||||||
@ -791,7 +793,7 @@ desktop/views/components/settings.profile.vue:
|
|||||||
birthday: "Date de naissance"
|
birthday: "Date de naissance"
|
||||||
save: "Mettre à jour le profil"
|
save: "Mettre à jour le profil"
|
||||||
locked-account: "Protéger votre compte"
|
locked-account: "Protéger votre compte"
|
||||||
is-locked: "フォローを承認制にする"
|
is-locked: "Demande d’abonnement en attente d’approbation"
|
||||||
other: "Autre"
|
other: "Autre"
|
||||||
is-bot: "Ce compte est un Bot"
|
is-bot: "Ce compte est un Bot"
|
||||||
is-cat: "Ce compte est un Chat"
|
is-cat: "Ce compte est un Chat"
|
||||||
@ -808,8 +810,13 @@ desktop/views/components/timeline.vue:
|
|||||||
local: "Local"
|
local: "Local"
|
||||||
hybrid: "Social"
|
hybrid: "Social"
|
||||||
global: "Global"
|
global: "Global"
|
||||||
mentions: "あなた宛て"
|
mentions: "Mentions"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "Listes"
|
list: "Listes"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "Content de vous revoir !"
|
welcome-back: "Content de vous revoir !"
|
||||||
adjective: "さん"
|
adjective: "さん"
|
||||||
@ -1134,7 +1141,8 @@ mobile/views/pages/home.vue:
|
|||||||
local: "Local"
|
local: "Local"
|
||||||
hybrid: "Social"
|
hybrid: "Social"
|
||||||
global: "Global"
|
global: "Global"
|
||||||
mentions: "あなた宛て"
|
mentions: "Mentions"
|
||||||
|
messages: "メッセージ"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "Pas de message avec un hashtag {} trouvé."
|
no-posts-found: "Pas de message avec un hashtag {} trouvé."
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
@ -1175,7 +1183,7 @@ mobile/views/pages/settings/settings.profile.vue:
|
|||||||
avatar: "Avatar"
|
avatar: "Avatar"
|
||||||
banner: "Bannière"
|
banner: "Bannière"
|
||||||
is-cat: "Ce compte est un Bot"
|
is-cat: "Ce compte est un Bot"
|
||||||
is-locked: "フォローを承認制にする"
|
is-locked: "Demande d’abonnement en attente d’approbation"
|
||||||
advanced: "Avancé"
|
advanced: "Avancé"
|
||||||
privacy: "Vie privée"
|
privacy: "Vie privée"
|
||||||
save: "Mettre à jour le profil"
|
save: "Mettre à jour le profil"
|
||||||
|
@ -155,8 +155,10 @@ common:
|
|||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
local: "ローカル"
|
local: "ローカル"
|
||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
swap-left: "左に移動"
|
swap-left: "左に移動"
|
||||||
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "おかえりなさい、"
|
welcome-back: "おかえりなさい、"
|
||||||
adjective: "さん"
|
adjective: "さん"
|
||||||
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
|
@ -166,8 +166,10 @@ common:
|
|||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
local: "ローカル"
|
local: "ローカル"
|
||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
swap-left: "左に移動"
|
swap-left: "左に移動"
|
||||||
@ -915,7 +917,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
|
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "おかえりなさい、"
|
welcome-back: "おかえりなさい、"
|
||||||
@ -1317,6 +1324,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
|
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||||
|
@ -155,8 +155,10 @@ common:
|
|||||||
home: "うち"
|
home: "うち"
|
||||||
local: "ローカル"
|
local: "ローカル"
|
||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
swap-left: "左に移動や!"
|
swap-left: "左に移動や!"
|
||||||
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "おかえり、"
|
welcome-back: "おかえり、"
|
||||||
adjective: "さん"
|
adjective: "さん"
|
||||||
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿はあらへんで。"
|
no-posts-found: "ハッシュタグ「{}」が付けられた投稿はあらへんで。"
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
|
@ -155,8 +155,10 @@ common:
|
|||||||
home: "홈"
|
home: "홈"
|
||||||
local: "로컬"
|
local: "로컬"
|
||||||
hybrid: "소셜"
|
hybrid: "소셜"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "글로벌"
|
global: "글로벌"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "통지"
|
notifications: "통지"
|
||||||
list: "목록"
|
list: "목록"
|
||||||
swap-left: "左に移動"
|
swap-left: "左に移動"
|
||||||
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "おかえりなさい、"
|
welcome-back: "おかえりなさい、"
|
||||||
adjective: "さん"
|
adjective: "さん"
|
||||||
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
|
@ -155,8 +155,10 @@ common:
|
|||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
local: "ローカル"
|
local: "ローカル"
|
||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
swap-left: "左に移動"
|
swap-left: "左に移動"
|
||||||
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "Algemeen"
|
global: "Algemeen"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "Lijsten"
|
list: "Lijsten"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "おかえりなさい、"
|
welcome-back: "おかえりなさい、"
|
||||||
adjective: "さん"
|
adjective: "さん"
|
||||||
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
|
@ -155,8 +155,10 @@ common:
|
|||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
local: "ローカル"
|
local: "ローカル"
|
||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
swap-left: "左に移動"
|
swap-left: "左に移動"
|
||||||
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "おかえりなさい、"
|
welcome-back: "おかえりなさい、"
|
||||||
adjective: "さん"
|
adjective: "さん"
|
||||||
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
|
@ -155,8 +155,10 @@ common:
|
|||||||
home: "Strona główna"
|
home: "Strona główna"
|
||||||
local: "Lokalne"
|
local: "Lokalne"
|
||||||
hybrid: "Społeczność"
|
hybrid: "Społeczność"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "Globalne"
|
global: "Globalne"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "Powiadomienia"
|
notifications: "Powiadomienia"
|
||||||
list: "Listy"
|
list: "Listy"
|
||||||
swap-left: "Przesuń w lewo"
|
swap-left: "Przesuń w lewo"
|
||||||
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "Społeczność"
|
hybrid: "Społeczność"
|
||||||
global: "Globalne"
|
global: "Globalne"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "Listy"
|
list: "Listy"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "Witaj ponownie,"
|
welcome-back: "Witaj ponownie,"
|
||||||
adjective: "さん"
|
adjective: "さん"
|
||||||
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "Społeczność"
|
hybrid: "Społeczność"
|
||||||
global: "Globalne"
|
global: "Globalne"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "Nie znaleziono wpisów zawierających „{}”."
|
no-posts-found: "Nie znaleziono wpisów zawierających „{}”."
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
|
@ -155,8 +155,10 @@ common:
|
|||||||
home: "Início"
|
home: "Início"
|
||||||
local: "Local"
|
local: "Local"
|
||||||
hybrid: "Social"
|
hybrid: "Social"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "Global"
|
global: "Global"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "Notificações"
|
notifications: "Notificações"
|
||||||
list: "Listas"
|
list: "Listas"
|
||||||
swap-left: "Mover para a esquerda"
|
swap-left: "Mover para a esquerda"
|
||||||
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "おかえりなさい、"
|
welcome-back: "おかえりなさい、"
|
||||||
adjective: "さん"
|
adjective: "さん"
|
||||||
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
|
@ -155,8 +155,10 @@ common:
|
|||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
local: "ローカル"
|
local: "ローカル"
|
||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
swap-left: "左に移動"
|
swap-left: "左に移動"
|
||||||
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "おかえりなさい、"
|
welcome-back: "おかえりなさい、"
|
||||||
adjective: "さん"
|
adjective: "さん"
|
||||||
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
|
@ -155,8 +155,10 @@ common:
|
|||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
local: "ローカル"
|
local: "ローカル"
|
||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト投稿"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
swap-left: "左に移動"
|
swap-left: "左に移動"
|
||||||
@ -809,7 +811,12 @@ desktop/views/components/timeline.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
add-tag-timeline: "ハッシュタグを追加"
|
||||||
|
add-list: "リストを追加"
|
||||||
|
list-name: "リスト名"
|
||||||
desktop/views/components/ui.header.vue:
|
desktop/views/components/ui.header.vue:
|
||||||
welcome-back: "おかえりなさい、"
|
welcome-back: "おかえりなさい、"
|
||||||
adjective: "さん"
|
adjective: "さん"
|
||||||
@ -1135,6 +1142,7 @@ mobile/views/pages/home.vue:
|
|||||||
hybrid: "ソーシャル"
|
hybrid: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
|
messages: "メッセージ"
|
||||||
mobile/views/pages/tag.vue:
|
mobile/views/pages/tag.vue:
|
||||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||||
mobile/views/pages/welcome.vue:
|
mobile/views/pages/welcome.vue:
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "8.44.0",
|
"version": "8.47.0",
|
||||||
"clientVersion": "1.0.9813",
|
"clientVersion": "1.0.9873",
|
||||||
"codename": "nighthike",
|
"codename": "nighthike",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
@ -60,7 +60,7 @@
|
|||||||
"@types/mocha": "5.2.3",
|
"@types/mocha": "5.2.3",
|
||||||
"@types/mongodb": "3.1.7",
|
"@types/mongodb": "3.1.7",
|
||||||
"@types/ms": "0.7.30",
|
"@types/ms": "0.7.30",
|
||||||
"@types/node": "10.9.4",
|
"@types/node": "10.10.1",
|
||||||
"@types/portscanner": "2.1.0",
|
"@types/portscanner": "2.1.0",
|
||||||
"@types/pug": "2.0.4",
|
"@types/pug": "2.0.4",
|
||||||
"@types/qrcode": "1.2.0",
|
"@types/qrcode": "1.2.0",
|
||||||
|
79
src/client/app/common/hotkey.ts
Normal file
79
src/client/app/common/hotkey.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import keyCode from './keycode';
|
||||||
|
|
||||||
|
const getKeyMap = keymap => Object.keys(keymap).map(input => {
|
||||||
|
const result = {} as any;
|
||||||
|
|
||||||
|
const { keyup, keydown } = keymap[input];
|
||||||
|
|
||||||
|
input.split('+').forEach(keyName => {
|
||||||
|
switch (keyName.toLowerCase()) {
|
||||||
|
case 'ctrl':
|
||||||
|
case 'alt':
|
||||||
|
case 'shift':
|
||||||
|
case 'meta':
|
||||||
|
result[keyName] = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result.keyCode = keyCode(keyName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
result.callback = {
|
||||||
|
keydown: keydown || keymap[input],
|
||||||
|
keyup
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ignoreElemens = ['input', 'textarea'];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(Vue) {
|
||||||
|
Vue.directive('hotkey', {
|
||||||
|
bind(el, binding) {
|
||||||
|
el._hotkey_global = binding.modifiers.global === true;
|
||||||
|
|
||||||
|
el._keymap = getKeyMap(binding.value);
|
||||||
|
|
||||||
|
el.dataset.reservedKeyCodes = el._keymap.map(key => `'${key.keyCode}'`).join(' ');
|
||||||
|
|
||||||
|
el._keyHandler = e => {
|
||||||
|
const reservedKeyCodes = document.activeElement ? ((document.activeElement as any).dataset || {}).reservedKeyCodes || '' : '';
|
||||||
|
if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
|
||||||
|
|
||||||
|
for (const hotkey of el._keymap) {
|
||||||
|
if (el._hotkey_global && reservedKeyCodes.includes(`'${e.keyCode}'`)) break;
|
||||||
|
|
||||||
|
const callback = hotkey.keyCode === e.keyCode &&
|
||||||
|
!!hotkey.ctrl === e.ctrlKey &&
|
||||||
|
!!hotkey.alt === e.altKey &&
|
||||||
|
!!hotkey.shift === e.shiftKey &&
|
||||||
|
!!hotkey.meta === e.metaKey &&
|
||||||
|
hotkey.callback[e.type];
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (el._hotkey_global) {
|
||||||
|
document.addEventListener('keydown', el._keyHandler);
|
||||||
|
} else {
|
||||||
|
el.addEventListener('keydown', el._keyHandler);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unbind(el) {
|
||||||
|
if (el._hotkey_global) {
|
||||||
|
document.removeEventListener('keydown', el._keyHandler);
|
||||||
|
} else {
|
||||||
|
el.removeEventListener('keydown', el._keyHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
139
src/client/app/common/keycode.ts
Normal file
139
src/client/app/common/keycode.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
export default searchInput => {
|
||||||
|
// Keyboard Events
|
||||||
|
if (searchInput && typeof searchInput === 'object') {
|
||||||
|
const hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode;
|
||||||
|
if (hasKeyCode) {
|
||||||
|
searchInput = hasKeyCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numbers
|
||||||
|
// if (typeof searchInput === 'number') {
|
||||||
|
// return names[searchInput]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Everything else (cast to string)
|
||||||
|
const search = String(searchInput);
|
||||||
|
|
||||||
|
// check codes
|
||||||
|
const foundNamedKeyCodes = codes[search.toLowerCase()];
|
||||||
|
if (foundNamedKeyCodes) {
|
||||||
|
return foundNamedKeyCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check aliases
|
||||||
|
const foundNamedKeyAliases = aliases[search.toLowerCase()];
|
||||||
|
if (foundNamedKeyAliases) {
|
||||||
|
return foundNamedKeyAliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
// weird character?
|
||||||
|
if (search.length === 1) {
|
||||||
|
return search.charCodeAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get by name
|
||||||
|
*
|
||||||
|
* exports.code['enter'] // => 13
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const codes = {
|
||||||
|
'backspace': 8,
|
||||||
|
'tab': 9,
|
||||||
|
'enter': 13,
|
||||||
|
'shift': 16,
|
||||||
|
'ctrl': 17,
|
||||||
|
'alt': 18,
|
||||||
|
'pause/break': 19,
|
||||||
|
'caps lock': 20,
|
||||||
|
'esc': 27,
|
||||||
|
'space': 32,
|
||||||
|
'page up': 33,
|
||||||
|
'page down': 34,
|
||||||
|
'end': 35,
|
||||||
|
'home': 36,
|
||||||
|
'left': 37,
|
||||||
|
'up': 38,
|
||||||
|
'right': 39,
|
||||||
|
'down': 40,
|
||||||
|
// 'add': 43,
|
||||||
|
'insert': 45,
|
||||||
|
'delete': 46,
|
||||||
|
'command': 91,
|
||||||
|
'left command': 91,
|
||||||
|
'right command': 93,
|
||||||
|
'numpad *': 106,
|
||||||
|
// 'numpad +': 107,
|
||||||
|
'numpad +': 43,
|
||||||
|
'numpad add': 43, // as a trick
|
||||||
|
'numpad -': 109,
|
||||||
|
'numpad .': 110,
|
||||||
|
'numpad /': 111,
|
||||||
|
'num lock': 144,
|
||||||
|
'scroll lock': 145,
|
||||||
|
'my computer': 182,
|
||||||
|
'my calculator': 183,
|
||||||
|
';': 186,
|
||||||
|
'=': 187,
|
||||||
|
',': 188,
|
||||||
|
'-': 189,
|
||||||
|
'.': 190,
|
||||||
|
'/': 191,
|
||||||
|
'`': 192,
|
||||||
|
'[': 219,
|
||||||
|
'\\': 220,
|
||||||
|
']': 221,
|
||||||
|
"'": 222
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper aliases
|
||||||
|
|
||||||
|
export const aliases = {
|
||||||
|
'windows': 91,
|
||||||
|
'⇧': 16,
|
||||||
|
'⌥': 18,
|
||||||
|
'⌃': 17,
|
||||||
|
'⌘': 91,
|
||||||
|
'ctl': 17,
|
||||||
|
'control': 17,
|
||||||
|
'option': 18,
|
||||||
|
'pause': 19,
|
||||||
|
'break': 19,
|
||||||
|
'caps': 20,
|
||||||
|
'return': 13,
|
||||||
|
'escape': 27,
|
||||||
|
'spc': 32,
|
||||||
|
'pgup': 33,
|
||||||
|
'pgdn': 34,
|
||||||
|
'ins': 45,
|
||||||
|
'del': 46,
|
||||||
|
'cmd': 91
|
||||||
|
};
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Programatically add the following
|
||||||
|
*/
|
||||||
|
|
||||||
|
// lower case chars
|
||||||
|
for (let i = 97; i < 123; i++) {
|
||||||
|
codes[String.fromCharCode(i)] = i - 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// numbers
|
||||||
|
for (let i = 48; i < 58; i++) {
|
||||||
|
codes[i - 48] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// function keys
|
||||||
|
for (let i = 1; i < 13; i++) {
|
||||||
|
codes['f' + i] = i + 111;
|
||||||
|
}
|
||||||
|
|
||||||
|
// numpad keys
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
codes['numpad ' + i] = i + 96;
|
||||||
|
}
|
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,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mk-reaction-picker">
|
<div class="mk-reaction-picker" v-hotkey.global="keymap">
|
||||||
<div class="backdrop" ref="backdrop" @click="close"></div>
|
<div class="backdrop" ref="backdrop" @click="close"></div>
|
||||||
<div class="popover" :class="{ compact, big }" ref="popover">
|
<div class="popover" :class="{ compact, big }" ref="popover">
|
||||||
<p v-if="!compact">{{ title }}</p>
|
<p v-if="!compact">{{ title }}</p>
|
||||||
@ -31,28 +31,51 @@ export default Vue.extend({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
|
||||||
source: {
|
source: {
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
|
||||||
compact: {
|
compact: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
cb: {
|
cb: {
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
|
||||||
big: {
|
big: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
title: placeholder
|
title: placeholder
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
keymap(): any {
|
||||||
|
return {
|
||||||
|
'1': () => this.react('like'),
|
||||||
|
'2': () => this.react('love'),
|
||||||
|
'3': () => this.react('laugh'),
|
||||||
|
'4': () => this.react('hmm'),
|
||||||
|
'5': () => this.react('surprise'),
|
||||||
|
'6': () => this.react('congrats'),
|
||||||
|
'7': () => this.react('angry'),
|
||||||
|
'8': () => this.react('confused'),
|
||||||
|
'9': () => this.react('rip'),
|
||||||
|
'0': () => this.react('pudding'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const popover = this.$refs.popover as any;
|
const popover = this.$refs.popover as any;
|
||||||
@ -88,6 +111,7 @@ export default Vue.extend({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
react(reaction) {
|
react(reaction) {
|
||||||
(this as any).api('notes/reactions/create', {
|
(this as any).api('notes/reactions/create', {
|
||||||
@ -95,15 +119,19 @@ export default Vue.extend({
|
|||||||
reaction: reaction
|
reaction: reaction
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
if (this.cb) this.cb();
|
if (this.cb) this.cb();
|
||||||
|
this.$emit('closed');
|
||||||
this.destroyDom();
|
this.destroyDom();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseover(e) {
|
onMouseover(e) {
|
||||||
this.title = e.target.title;
|
this.title = e.target.title;
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseout(e) {
|
onMouseout(e) {
|
||||||
this.title = placeholder;
|
this.title = placeholder;
|
||||||
},
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
(this.$refs.backdrop as any).style.pointerEvents = 'none';
|
(this.$refs.backdrop as any).style.pointerEvents = 'none';
|
||||||
anime({
|
anime({
|
||||||
@ -120,7 +148,10 @@ export default Vue.extend({
|
|||||||
scale: 0.5,
|
scale: 0.5,
|
||||||
duration: 200,
|
duration: 200,
|
||||||
easing: 'easeInBack',
|
easing: 'easeInBack',
|
||||||
complete: () => this.destroyDom()
|
complete: () => {
|
||||||
|
this.$emit('closed');
|
||||||
|
this.destroyDom();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy">
|
<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
|
||||||
<span slot="header">
|
<span slot="header">
|
||||||
<span v-html="title" :class="$style.title"></span>
|
<span v-html="title" :class="$style.title"></span>
|
||||||
<span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}%i18n:@choose-file%)</span>
|
<span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}%i18n:@choose-file%)</span>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy">
|
<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
|
||||||
<span slot="header">
|
<span slot="header">
|
||||||
<span v-html="title" :class="$style.title"></span>
|
<span v-html="title" :class="$style.title"></span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout-url="popout">
|
<mk-window ref="window" @closed="destroyDom" width="800px" height="500px" :popout-url="popout">
|
||||||
<template slot="header">
|
<template slot="header">
|
||||||
<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:@used%</p>
|
<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:@used%</p>
|
||||||
<span :class="$style.title">%fa:cloud%%i18n:@drive%</span>
|
<span :class="$style.title">%fa:cloud%%i18n:@drive%</span>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window width="400px" height="550px" @closed="$destroy">
|
<mk-window width="400px" height="550px" @closed="destroyDom">
|
||||||
<span slot="header" :class="$style.header">
|
<span slot="header" :class="$style.header">
|
||||||
<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }}
|
<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window width="400px" height="550px" @closed="$destroy">
|
<mk-window width="400px" height="550px" @closed="destroyDom">
|
||||||
<span slot="header" :class="$style.header">
|
<span slot="header" :class="$style.header">
|
||||||
<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }}
|
<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
|
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
|
||||||
<span slot="header" :class="$style.header">%fa:gamepad%%i18n:@game%</span>
|
<span slot="header" :class="$style.header">%fa:gamepad%%i18n:@game%</span>
|
||||||
<mk-reversi :class="$style.content" @gamed="g => game = g"/>
|
<mk-reversi :class="$style.content" @gamed="g => game = g"/>
|
||||||
</mk-window>
|
</mk-window>
|
||||||
|
@ -237,6 +237,10 @@ export default Vue.extend({
|
|||||||
|
|
||||||
warp(date) {
|
warp(date) {
|
||||||
(this.$refs.tl as any).warp(date);
|
(this.$refs.tl as any).warp(date);
|
||||||
|
},
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
(this.$refs.tl as any).focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="$destroy">
|
<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="destroyDom">
|
||||||
<span slot="header" :class="$style.header">
|
<span slot="header" :class="$style.header">
|
||||||
%fa:i-cursor%{{ title }}
|
%fa:i-cursor%{{ title }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
|
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
|
||||||
<span slot="header" :class="$style.header">%fa:comments%%i18n:@title% {{ user | userName }}</span>
|
<span slot="header" :class="$style.header">%fa:comments%%i18n:@title% {{ user | userName }}</span>
|
||||||
<mk-messaging-room :user="user" :class="$style.content"/>
|
<mk-messaging-room :user="user" :class="$style.content"/>
|
||||||
</mk-window>
|
</mk-window>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" width="500px" height="560px" @closed="$destroy">
|
<mk-window ref="window" width="500px" height="560px" @closed="destroyDom">
|
||||||
<span slot="header" :class="$style.header">%fa:comments%%i18n:@title%</span>
|
<span slot="header" :class="$style.header">%fa:comments%%i18n:@title%</span>
|
||||||
<mk-messaging :class="$style.content" @navigate="navigate"/>
|
<mk-messaging :class="$style.content" @navigate="navigate"/>
|
||||||
</mk-window>
|
</mk-window>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
|
<div class="note" tabindex="-1" v-hotkey="keymap" :title="title">
|
||||||
<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
|
<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
|
||||||
<x-sub :note="p.reply"/>
|
<x-sub :note="p.reply"/>
|
||||||
</div>
|
</div>
|
||||||
@ -111,6 +111,18 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
keymap(): any {
|
||||||
|
return {
|
||||||
|
'r': this.reply,
|
||||||
|
'a': this.react,
|
||||||
|
'n': this.renote,
|
||||||
|
'up': this.focusBefore,
|
||||||
|
'shift+tab': this.focusBefore,
|
||||||
|
'down': this.focusAfter,
|
||||||
|
'tab': this.focusAfter,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
isRenote(): boolean {
|
isRenote(): boolean {
|
||||||
return (this.note.renote &&
|
return (this.note.renote &&
|
||||||
this.note.text == null &&
|
this.note.text == null &&
|
||||||
@ -223,64 +235,39 @@ export default Vue.extend({
|
|||||||
reply() {
|
reply() {
|
||||||
(this as any).os.new(MkPostFormWindow, {
|
(this as any).os.new(MkPostFormWindow, {
|
||||||
reply: this.p
|
reply: this.p
|
||||||
});
|
}).$once('closed', this.focus);
|
||||||
},
|
},
|
||||||
|
|
||||||
renote() {
|
renote() {
|
||||||
(this as any).os.new(MkRenoteFormWindow, {
|
(this as any).os.new(MkRenoteFormWindow, {
|
||||||
note: this.p
|
note: this.p
|
||||||
});
|
}).$once('closed', this.focus);
|
||||||
},
|
},
|
||||||
|
|
||||||
react() {
|
react() {
|
||||||
(this as any).os.new(MkReactionPicker, {
|
(this as any).os.new(MkReactionPicker, {
|
||||||
source: this.$refs.reactButton,
|
source: this.$refs.reactButton,
|
||||||
note: this.p
|
note: this.p
|
||||||
});
|
}).$once('closed', this.focus);
|
||||||
},
|
},
|
||||||
|
|
||||||
menu() {
|
menu() {
|
||||||
(this as any).os.new(MkNoteMenu, {
|
(this as any).os.new(MkNoteMenu, {
|
||||||
source: this.$refs.menuButton,
|
source: this.$refs.menuButton,
|
||||||
note: this.p
|
note: this.p
|
||||||
});
|
}).$once('closed', this.focus);
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeydown(e) {
|
focus() {
|
||||||
let shouldBeCancel = true;
|
this.$el.focus();
|
||||||
|
},
|
||||||
|
|
||||||
switch (true) {
|
focusBefore() {
|
||||||
case e.which == 38: // [↑]
|
|
||||||
case e.which == 74: // [j]
|
|
||||||
case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
|
|
||||||
focus(this.$el, e => e.previousElementSibling);
|
focus(this.$el, e => e.previousElementSibling);
|
||||||
break;
|
},
|
||||||
|
|
||||||
case e.which == 40: // [↓]
|
focusAfter() {
|
||||||
case e.which == 75: // [k]
|
|
||||||
case e.which == 9: // [Tab]
|
|
||||||
focus(this.$el, e => e.nextElementSibling);
|
focus(this.$el, e => e.nextElementSibling);
|
||||||
break;
|
|
||||||
|
|
||||||
case e.which == 81: // [q]
|
|
||||||
case e.which == 69: // [e]
|
|
||||||
this.renote();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case e.which == 70: // [f]
|
|
||||||
case e.which == 76: // [l]
|
|
||||||
//this.like();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case e.which == 82: // [r]
|
|
||||||
this.reply();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
shouldBeCancel = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldBeCancel) e.preventDefault();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div">
|
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div">
|
||||||
<template v-for="(note, i) in _notes">
|
<template v-for="(note, i) in _notes">
|
||||||
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
|
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" ref="note"/>
|
||||||
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
||||||
<span>%fa:angle-up%{{ note._datetext }}</span>
|
<span>%fa:angle-up%{{ note._datetext }}</span>
|
||||||
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
|
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
|
||||||
@ -89,7 +89,7 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
(this.$el as any).children[0].focus();
|
(this.$refs.note as any)[0].focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
onNoteUpdated(i, note) {
|
onNoteUpdated(i, note) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window class="mk-post-form-window" ref="window" is-modal @closed="$destroy">
|
<mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed">
|
||||||
<span slot="header" class="mk-post-form-window--header">
|
<span slot="header" class="mk-post-form-window--header">
|
||||||
<span class="icon" v-if="geo">%fa:map-marker-alt%</span>
|
<span class="icon" v-if="geo">%fa:map-marker-alt%</span>
|
||||||
<span v-if="!reply">%i18n:@note%</span>
|
<span v-if="!reply">%i18n:@note%</span>
|
||||||
@ -53,6 +53,10 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
onPosted() {
|
onPosted() {
|
||||||
(this.$refs.window as any).close();
|
(this.$refs.window as any).close();
|
||||||
|
},
|
||||||
|
onWindowClosed() {
|
||||||
|
this.$emit('closed');
|
||||||
|
this.destroyDom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="$destroy">
|
<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="destroyDom">
|
||||||
<span slot="header">{{ title }}<mk-ellipsis/></span>
|
<span slot="header">{{ title }}<mk-ellipsis/></span>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<p :class="$style.init" v-if="isNaN(value)">%i18n:@waiting%<mk-ellipsis/></p>
|
<p :class="$style.init" v-if="isNaN(value)">%i18n:@waiting%<mk-ellipsis/></p>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
|
<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
|
||||||
<span slot="header">%fa:envelope R% %i18n:@title%</span>
|
<span slot="header">%fa:envelope R% %i18n:@title%</span>
|
||||||
|
|
||||||
<div class="slpqaxdoxhvglersgjukmvizkqbmbokc" :data-darkmode="$store.state.device.darkmode">
|
<div class="slpqaxdoxhvglersgjukmvizkqbmbokc" :data-darkmode="$store.state.device.darkmode">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<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>
|
<span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span>
|
||||||
<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/>
|
<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/>
|
||||||
</mk-window>
|
</mk-window>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -10,25 +10,32 @@ import Vue from 'vue';
|
|||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: ['note'],
|
props: ['note'],
|
||||||
mounted() {
|
|
||||||
document.addEventListener('keydown', this.onDocumentKeydown);
|
computed: {
|
||||||
},
|
keymap(): any {
|
||||||
beforeDestroy() {
|
return {
|
||||||
document.removeEventListener('keydown', this.onDocumentKeydown);
|
'esc': this.close,
|
||||||
|
'ctrl+enter': this.post
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onDocumentKeydown(e) {
|
post() {
|
||||||
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
|
(this.$refs.form as any).ok();
|
||||||
if (e.which == 27) { // Esc
|
},
|
||||||
|
close() {
|
||||||
(this.$refs.window as any).close();
|
(this.$refs.window as any).close();
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onPosted() {
|
onPosted() {
|
||||||
(this.$refs.window as any).close();
|
(this.$refs.window as any).close();
|
||||||
},
|
},
|
||||||
onCanceled() {
|
onCanceled() {
|
||||||
(this.$refs.window as any).close();
|
(this.$refs.window as any).close();
|
||||||
|
},
|
||||||
|
onWindowClosed() {
|
||||||
|
this.$emit('closed');
|
||||||
|
this.destroyDom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy">
|
<mk-window ref="window" is-modal width="700px" height="550px" @closed="destroyDom">
|
||||||
<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span>
|
<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span>
|
||||||
<mk-settings @done="close"/>
|
<mk-settings :initial-page="initialPage" @done="close"/>
|
||||||
</mk-window>
|
</mk-window>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
initialPage: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
close() {
|
close() {
|
||||||
(this as any).$refs.window.close();
|
(this as any).$refs.window.close();
|
||||||
|
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 == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p>
|
||||||
<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p>
|
<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p>
|
||||||
<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p>
|
<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p>
|
||||||
|
<p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'">%fa:hashtag .fw%%i18n:@tags%</p>
|
||||||
<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p>
|
<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p>
|
||||||
<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p>
|
<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p>
|
||||||
<p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p>
|
<p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p>
|
||||||
@ -138,6 +139,11 @@
|
|||||||
<x-drive/>
|
<x-drive/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="hashtags" v-show="page == 'hashtags'">
|
||||||
|
<h1>%i18n:@tags%</h1>
|
||||||
|
<x-tags/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="mute" v-show="page == 'mute'">
|
<section class="mute" v-show="page == 'mute'">
|
||||||
<h1>%i18n:@mute%</h1>
|
<h1>%i18n:@mute%</h1>
|
||||||
<x-mute/>
|
<x-mute/>
|
||||||
@ -222,6 +228,7 @@ import XApi from './settings.api.vue';
|
|||||||
import XApps from './settings.apps.vue';
|
import XApps from './settings.apps.vue';
|
||||||
import XSignins from './settings.signins.vue';
|
import XSignins from './settings.signins.vue';
|
||||||
import XDrive from './settings.drive.vue';
|
import XDrive from './settings.drive.vue';
|
||||||
|
import XTags from './settings.tags.vue';
|
||||||
import { url, langs, version } from '../../../config';
|
import { url, langs, version } from '../../../config';
|
||||||
import checkForUpdate from '../../../common/scripts/check-for-update';
|
import checkForUpdate from '../../../common/scripts/check-for-update';
|
||||||
|
|
||||||
@ -234,11 +241,18 @@ export default Vue.extend({
|
|||||||
XApi,
|
XApi,
|
||||||
XApps,
|
XApps,
|
||||||
XSignins,
|
XSignins,
|
||||||
XDrive
|
XDrive,
|
||||||
|
XTags
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
initialPage: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
page: 'profile',
|
page: this.initialPage || 'profile',
|
||||||
meta: null,
|
meta: null,
|
||||||
version,
|
version,
|
||||||
langs,
|
langs,
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
|
||||||
|
|
||||||
const fetchLimit = 10;
|
const fetchLimit = 10;
|
||||||
|
|
||||||
@ -23,6 +24,9 @@ export default Vue.extend({
|
|||||||
src: {
|
src: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
tagTl: {
|
||||||
|
required: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -31,9 +35,17 @@ export default Vue.extend({
|
|||||||
fetching: true,
|
fetching: true,
|
||||||
moreFetching: false,
|
moreFetching: false,
|
||||||
existMore: false,
|
existMore: false,
|
||||||
|
streamManager: null,
|
||||||
connection: null,
|
connection: null,
|
||||||
connectionId: null,
|
connectionId: null,
|
||||||
date: null
|
date: null,
|
||||||
|
baseQuery: {
|
||||||
|
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||||
|
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||||
|
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
endpoint: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -42,55 +54,109 @@ export default Vue.extend({
|
|||||||
return this.$store.state.i.followingCount == 0;
|
return this.$store.state.i.followingCount == 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
stream(): any {
|
|
||||||
switch (this.src) {
|
|
||||||
case 'home': return (this as any).os.stream;
|
|
||||||
case 'local': return (this as any).os.streams.localTimelineStream;
|
|
||||||
case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
|
|
||||||
case 'global': return (this as any).os.streams.globalTimelineStream;
|
|
||||||
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 {
|
canFetchMore(): boolean {
|
||||||
return !this.moreFetching && !this.fetching && this.existMore;
|
return !this.moreFetching && !this.fetching && this.existMore;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.connection = this.stream.getConnection();
|
const prepend = note => {
|
||||||
this.connectionId = this.stream.use();
|
(this.$refs.timeline as any).prepend(note);
|
||||||
|
};
|
||||||
|
|
||||||
this.connection.on(this.src == 'mentions' ? 'mention' : 'note', this.onNote);
|
if (this.src == 'tag') {
|
||||||
if (this.src == 'home') {
|
this.endpoint = 'notes/search_by_tag';
|
||||||
this.connection.on('follow', this.onChangeFollowing);
|
this.query = {
|
||||||
this.connection.on('unfollow', this.onChangeFollowing);
|
query: this.tagTl.query
|
||||||
|
};
|
||||||
|
this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
|
||||||
|
this.connection.on('note', prepend);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('note', prepend);
|
||||||
|
this.connection.close();
|
||||||
|
});
|
||||||
|
} else if (this.src == 'home') {
|
||||||
|
this.endpoint = 'notes/timeline';
|
||||||
|
const onChangeFollowing = () => {
|
||||||
|
this.fetch();
|
||||||
|
};
|
||||||
|
this.streamManager = (this as any).os.stream;
|
||||||
|
this.connection = this.streamManager.getConnection();
|
||||||
|
this.connectionId = this.streamManager.use();
|
||||||
|
this.connection.on('note', prepend);
|
||||||
|
this.connection.on('follow', onChangeFollowing);
|
||||||
|
this.connection.on('unfollow', onChangeFollowing);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('note', prepend);
|
||||||
|
this.connection.off('follow', onChangeFollowing);
|
||||||
|
this.connection.off('unfollow', onChangeFollowing);
|
||||||
|
this.streamManager.dispose(this.connectionId);
|
||||||
|
});
|
||||||
|
} else if (this.src == 'local') {
|
||||||
|
this.endpoint = 'notes/local-timeline';
|
||||||
|
this.streamManager = (this as any).os.streams.localTimelineStream;
|
||||||
|
this.connection = this.streamManager.getConnection();
|
||||||
|
this.connectionId = this.streamManager.use();
|
||||||
|
this.connection.on('note', prepend);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('note', prepend);
|
||||||
|
this.streamManager.dispose(this.connectionId);
|
||||||
|
});
|
||||||
|
} else if (this.src == 'hybrid') {
|
||||||
|
this.endpoint = 'notes/hybrid-timeline';
|
||||||
|
this.streamManager = (this as any).os.streams.hybridTimelineStream;
|
||||||
|
this.connection = this.streamManager.getConnection();
|
||||||
|
this.connectionId = this.streamManager.use();
|
||||||
|
this.connection.on('note', prepend);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('note', prepend);
|
||||||
|
this.streamManager.dispose(this.connectionId);
|
||||||
|
});
|
||||||
|
} else if (this.src == 'global') {
|
||||||
|
this.endpoint = 'notes/global-timeline';
|
||||||
|
this.streamManager = (this as any).os.streams.globalTimelineStream;
|
||||||
|
this.connection = this.streamManager.getConnection();
|
||||||
|
this.connectionId = this.streamManager.use();
|
||||||
|
this.connection.on('note', prepend);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('note', prepend);
|
||||||
|
this.streamManager.dispose(this.connectionId);
|
||||||
|
});
|
||||||
|
} else if (this.src == 'mentions') {
|
||||||
|
this.endpoint = 'notes/mentions';
|
||||||
|
this.streamManager = (this as any).os.stream;
|
||||||
|
this.connection = this.streamManager.getConnection();
|
||||||
|
this.connectionId = this.streamManager.use();
|
||||||
|
this.connection.on('mention', prepend);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('mention', prepend);
|
||||||
|
this.streamManager.dispose(this.connectionId);
|
||||||
|
});
|
||||||
|
} else if (this.src == 'messages') {
|
||||||
|
this.endpoint = 'notes/mentions';
|
||||||
|
this.query = {
|
||||||
|
visibility: 'specified'
|
||||||
|
};
|
||||||
|
const onNote = note => {
|
||||||
|
if (note.visibility == 'specified') {
|
||||||
|
prepend(note);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.streamManager = (this as any).os.stream;
|
||||||
|
this.connection = this.streamManager.getConnection();
|
||||||
|
this.connectionId = this.streamManager.use();
|
||||||
|
this.connection.on('mention', onNote);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('mention', onNote);
|
||||||
|
this.streamManager.dispose(this.connectionId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', this.onKeydown);
|
|
||||||
|
|
||||||
this.fetch();
|
this.fetch();
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.connection.off(this.src == 'mentions' ? 'mention' : 'note', this.onNote);
|
this.$emit('beforeDestroy');
|
||||||
if (this.src == 'home') {
|
|
||||||
this.connection.off('follow', this.onChangeFollowing);
|
|
||||||
this.connection.off('unfollow', this.onChangeFollowing);
|
|
||||||
}
|
|
||||||
this.stream.dispose(this.connectionId);
|
|
||||||
|
|
||||||
document.removeEventListener('keydown', this.onKeydown);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@ -98,13 +164,10 @@ export default Vue.extend({
|
|||||||
this.fetching = true;
|
this.fetching = true;
|
||||||
|
|
||||||
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
|
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
|
||||||
(this as any).api(this.endpoint, {
|
(this as any).api(this.endpoint, Object.assign({
|
||||||
limit: fetchLimit + 1,
|
limit: fetchLimit + 1,
|
||||||
untilDate: this.date ? this.date.getTime() : undefined,
|
untilDate: this.date ? this.date.getTime() : undefined
|
||||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
}, this.baseQuery, this.query)).then(notes => {
|
||||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
|
||||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
|
||||||
}).then(notes => {
|
|
||||||
if (notes.length == fetchLimit + 1) {
|
if (notes.length == fetchLimit + 1) {
|
||||||
notes.pop();
|
notes.pop();
|
||||||
this.existMore = true;
|
this.existMore = true;
|
||||||
@ -121,13 +184,10 @@ export default Vue.extend({
|
|||||||
|
|
||||||
this.moreFetching = true;
|
this.moreFetching = true;
|
||||||
|
|
||||||
const promise = (this as any).api(this.endpoint, {
|
const promise = (this as any).api(this.endpoint, Object.assign({
|
||||||
limit: fetchLimit + 1,
|
limit: fetchLimit + 1,
|
||||||
untilId: (this.$refs.timeline as any).tail().id,
|
untilId: (this.$refs.timeline as any).tail().id
|
||||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
}, this.baseQuery, this.query));
|
||||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
|
||||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.then(notes => {
|
promise.then(notes => {
|
||||||
if (notes.length == fetchLimit + 1) {
|
if (notes.length == fetchLimit + 1) {
|
||||||
@ -142,15 +202,6 @@ export default Vue.extend({
|
|||||||
return promise;
|
return promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
onNote(note) {
|
|
||||||
// Prepend a note
|
|
||||||
(this.$refs.timeline as any).prepend(note);
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeFollowing() {
|
|
||||||
this.fetch();
|
|
||||||
},
|
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
(this.$refs.timeline as any).focus();
|
(this.$refs.timeline as any).focus();
|
||||||
},
|
},
|
||||||
@ -158,14 +209,6 @@ export default Vue.extend({
|
|||||||
warp(date) {
|
warp(date) {
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.fetch();
|
this.fetch();
|
||||||
},
|
|
||||||
|
|
||||||
onKeydown(e) {
|
|
||||||
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
|
|
||||||
if (e.which == 84) { // t
|
|
||||||
this.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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 == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
|
||||||
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
|
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
|
||||||
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
|
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
|
||||||
<span :data-active="src == '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>
|
<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>
|
</header>
|
||||||
<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/>
|
<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/>
|
||||||
<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/>
|
<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/>
|
||||||
<x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
|
<x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
|
||||||
<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/>
|
<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/>
|
||||||
<x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
|
<x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
|
||||||
|
<x-core v-if="src == 'messages'" ref="tl" key="messages" src="messages"/>
|
||||||
|
<x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/>
|
||||||
<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
|
<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -21,7 +28,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import XCore from './timeline.core.vue';
|
import XCore from './timeline.core.vue';
|
||||||
import MkUserListsWindow from './user-lists-window.vue';
|
import Menu from '../../../common/views/components/menu.vue';
|
||||||
|
import MkSettingsWindow from './settings-window.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -32,6 +40,7 @@ export default Vue.extend({
|
|||||||
return {
|
return {
|
||||||
src: 'home',
|
src: 'home',
|
||||||
list: null,
|
list: null,
|
||||||
|
tagTl: null,
|
||||||
enableLocalTimeline: false
|
enableLocalTimeline: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -41,8 +50,14 @@ export default Vue.extend({
|
|||||||
this.saveSrc();
|
this.saveSrc();
|
||||||
},
|
},
|
||||||
|
|
||||||
list() {
|
list(x) {
|
||||||
this.saveSrc();
|
this.saveSrc();
|
||||||
|
if (x != null) this.tagTl = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
tagTl(x) {
|
||||||
|
this.saveSrc();
|
||||||
|
if (x != null) this.list = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -55,6 +70,8 @@ export default Vue.extend({
|
|||||||
this.src = this.$store.state.device.tl.src;
|
this.src = this.$store.state.device.tl.src;
|
||||||
if (this.src == 'list') {
|
if (this.src == 'list') {
|
||||||
this.list = this.$store.state.device.tl.arg;
|
this.list = this.$store.state.device.tl.arg;
|
||||||
|
} else if (this.src == 'tag') {
|
||||||
|
this.tagTl = this.$store.state.device.tl.arg;
|
||||||
}
|
}
|
||||||
} else if (this.$store.state.i.followingCount == 0) {
|
} else if (this.$store.state.i.followingCount == 0) {
|
||||||
this.src = 'hybrid';
|
this.src = 'hybrid';
|
||||||
@ -71,20 +88,86 @@ export default Vue.extend({
|
|||||||
saveSrc() {
|
saveSrc() {
|
||||||
this.$store.commit('device/setTl', {
|
this.$store.commit('device/setTl', {
|
||||||
src: this.src,
|
src: this.src,
|
||||||
arg: this.list
|
arg: this.src == 'list' ? this.list : this.tagTl
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
(this.$refs.tl as any).focus();
|
||||||
|
},
|
||||||
|
|
||||||
warp(date) {
|
warp(date) {
|
||||||
(this.$refs.tl as any).warp(date);
|
(this.$refs.tl as any).warp(date);
|
||||||
},
|
},
|
||||||
|
|
||||||
chooseList() {
|
async chooseList() {
|
||||||
const w = (this as any).os.new(MkUserListsWindow);
|
const lists = await (this as any).api('users/lists/list');
|
||||||
w.$once('choosen', list => {
|
|
||||||
|
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.list = list;
|
||||||
this.src = 'list';
|
this.src = 'list';
|
||||||
w.close();
|
});
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
|
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,13 +189,15 @@ root(isDark)
|
|||||||
border-radius 6px 6px 0 0
|
border-radius 6px 6px 0 0
|
||||||
box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
|
box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
|
||||||
|
|
||||||
> button
|
> .buttons
|
||||||
position absolute
|
position absolute
|
||||||
z-index 2
|
z-index 2
|
||||||
top 0
|
top 0
|
||||||
right 0
|
right 0
|
||||||
padding 0
|
padding-right 8px
|
||||||
width 42px
|
|
||||||
|
> button
|
||||||
|
padding 0 8px
|
||||||
font-size 0.9em
|
font-size 0.9em
|
||||||
line-height 42px
|
line-height 42px
|
||||||
color isDark ? #9baec8 : #ccc
|
color isDark ? #9baec8 : #ccc
|
||||||
@ -123,6 +208,20 @@ root(isDark)
|
|||||||
&:active
|
&:active
|
||||||
color isDark ? #b2c1d5 : #999
|
color isDark ? #b2c1d5 : #999
|
||||||
|
|
||||||
|
&[data-active]
|
||||||
|
color $theme-color
|
||||||
|
cursor default
|
||||||
|
|
||||||
|
&:before
|
||||||
|
content ""
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
width 100%
|
||||||
|
height 2px
|
||||||
|
background $theme-color
|
||||||
|
|
||||||
> span
|
> span
|
||||||
display inline-block
|
display inline-block
|
||||||
padding 0 10px
|
padding 0 10px
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mk-ui" :style="style">
|
<div class="mk-ui" :style="style" v-hotkey.global="keymap">
|
||||||
<x-header class="header" v-show="!zenMode"/>
|
<x-header class="header" v-show="!zenMode"/>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
@ -16,11 +16,13 @@ export default Vue.extend({
|
|||||||
components: {
|
components: {
|
||||||
XHeader
|
XHeader
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
zenMode: false
|
zenMode: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
style(): any {
|
style(): any {
|
||||||
if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {};
|
if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {};
|
||||||
@ -28,29 +30,26 @@ export default Vue.extend({
|
|||||||
backgroundColor: this.$store.state.i.wallpaperColor && this.$store.state.i.wallpaperColor.length == 3 ? `rgb(${ this.$store.state.i.wallpaperColor.join(',') })` : null,
|
backgroundColor: this.$store.state.i.wallpaperColor && this.$store.state.i.wallpaperColor.length == 3 ? `rgb(${ this.$store.state.i.wallpaperColor.join(',') })` : null,
|
||||||
backgroundImage: `url(${ this.$store.state.i.wallpaperUrl })`
|
backgroundImage: `url(${ this.$store.state.i.wallpaperUrl })`
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
keymap(): any {
|
||||||
|
return {
|
||||||
|
'p': this.post,
|
||||||
|
'n': this.post,
|
||||||
|
'z': this.toggleZenMode
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
document.addEventListener('keydown', this.onKeydown);
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
document.removeEventListener('keydown', this.onKeydown);
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
onKeydown(e) {
|
post() {
|
||||||
if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return;
|
|
||||||
|
|
||||||
if (e.which == 80 || e.which == 78) { // p or n
|
|
||||||
e.preventDefault();
|
|
||||||
(this as any).apis.post();
|
(this as any).apis.post();
|
||||||
}
|
},
|
||||||
|
|
||||||
if (e.which == 90) { // z
|
toggleZenMode() {
|
||||||
e.preventDefault();
|
|
||||||
this.zenMode = !this.zenMode;
|
this.zenMode = !this.zenMode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
|
<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
|
||||||
<span slot="header">%fa:list% %i18n:@title%</span>
|
<span slot="header">%fa:list% %i18n:@title%</span>
|
||||||
|
|
||||||
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp" :data-darkmode="$store.state.device.darkmode">
|
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp" :data-darkmode="$store.state.device.darkmode">
|
||||||
|
@ -190,8 +190,8 @@ export default Vue.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.destroyDom();
|
|
||||||
this.$emit('closed');
|
this.$emit('closed');
|
||||||
|
this.destroyDom();
|
||||||
}, 300);
|
}, 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 == 'hybrid'" :column="column" :is-stacked="isStacked"/>
|
||||||
<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/>
|
<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/>
|
||||||
<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/>
|
<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/>
|
||||||
|
<x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked"/>
|
||||||
<x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked"/>
|
<x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked"/>
|
||||||
|
<x-direct-column v-else-if="column.type == 'direct'" :column="column" :is-stacked="isStacked"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -15,13 +17,15 @@ import XTlColumn from './deck.tl-column.vue';
|
|||||||
import XNotificationsColumn from './deck.notifications-column.vue';
|
import XNotificationsColumn from './deck.notifications-column.vue';
|
||||||
import XWidgetsColumn from './deck.widgets-column.vue';
|
import XWidgetsColumn from './deck.widgets-column.vue';
|
||||||
import XMentionsColumn from './deck.mentions-column.vue';
|
import XMentionsColumn from './deck.mentions-column.vue';
|
||||||
|
import XDirectColumn from './deck.direct-column.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
XTlColumn,
|
XTlColumn,
|
||||||
XNotificationsColumn,
|
XNotificationsColumn,
|
||||||
XWidgetsColumn,
|
XWidgetsColumn,
|
||||||
XMentionsColumn
|
XMentionsColumn,
|
||||||
|
XDirectColumn
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
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 == 'hybrid'">%fa:share-alt%</template>
|
||||||
<template v-if="column.type == 'global'">%fa:globe%</template>
|
<template v-if="column.type == 'global'">%fa:globe%</template>
|
||||||
<template v-if="column.type == 'list'">%fa:list%</template>
|
<template v-if="column.type == 'list'">%fa:list%</template>
|
||||||
|
<template v-if="column.type == 'hashtag'">%fa:hashtag%</template>
|
||||||
<span>{{ name }}</span>
|
<span>{{ name }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@ -14,6 +15,7 @@
|
|||||||
<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/>
|
<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/>
|
||||||
</div>
|
</div>
|
||||||
<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
|
<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
|
||||||
|
<x-hashtag-tl v-if="column.type == 'hashtag'" :tag-tl="$store.state.settings.tagTimelines.find(x => x.id == column.tagTlId)" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
|
||||||
<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
|
<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
|
||||||
</x-column>
|
</x-column>
|
||||||
</template>
|
</template>
|
||||||
@ -23,12 +25,14 @@ import Vue from 'vue';
|
|||||||
import XColumn from './deck.column.vue';
|
import XColumn from './deck.column.vue';
|
||||||
import XTl from './deck.tl.vue';
|
import XTl from './deck.tl.vue';
|
||||||
import XListTl from './deck.list-tl.vue';
|
import XListTl from './deck.list-tl.vue';
|
||||||
|
import XHashtagTl from './deck.hashtag-tl.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
XColumn,
|
XColumn,
|
||||||
XTl,
|
XTl,
|
||||||
XListTl
|
XListTl,
|
||||||
|
XHashtagTl
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@ -65,6 +69,7 @@ export default Vue.extend({
|
|||||||
case 'hybrid': return '%i18n:common.deck.hybrid%';
|
case 'hybrid': return '%i18n:common.deck.hybrid%';
|
||||||
case 'global': return '%i18n:common.deck.global%';
|
case 'global': return '%i18n:common.deck.global%';
|
||||||
case 'list': return this.column.list.title;
|
case 'list': return this.column.list.title;
|
||||||
|
case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -147,6 +147,15 @@ export default Vue.extend({
|
|||||||
type: 'mentions'
|
type: 'mentions'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
icon: '%fa:envelope R%',
|
||||||
|
text: '%i18n:common.deck.direct%',
|
||||||
|
action: () => {
|
||||||
|
this.$store.dispatch('settings/addDeckColumn', {
|
||||||
|
id: uuid(),
|
||||||
|
type: 'direct'
|
||||||
|
});
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
icon: '%fa:list%',
|
icon: '%fa:list%',
|
||||||
text: '%i18n:common.deck.list%',
|
text: '%i18n:common.deck.list%',
|
||||||
@ -161,6 +170,20 @@ export default Vue.extend({
|
|||||||
w.close();
|
w.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
icon: '%fa:hashtag%',
|
||||||
|
text: '%i18n:common.deck.hashtag%',
|
||||||
|
action: () => {
|
||||||
|
(this as any).apis.input({
|
||||||
|
title: '%i18n:@enter-hashtag-tl-title%'
|
||||||
|
}).then(title => {
|
||||||
|
this.$store.dispatch('settings/addDeckColumn', {
|
||||||
|
id: uuid(),
|
||||||
|
type: 'hashtag',
|
||||||
|
tagTlId: this.$store.state.settings.tagTimelines.find(x => x.title == title).id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
icon: '%fa:bell R%',
|
icon: '%fa:bell R%',
|
||||||
text: '%i18n:common.deck.notifications%',
|
text: '%i18n:common.deck.notifications%',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<mk-home :mode="mode" @loaded="loaded"/>
|
<mk-home :mode="mode" @loaded="loaded" ref="home" v-hotkey.global="keymap"/>
|
||||||
</mk-ui>
|
</mk-ui>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -15,6 +15,13 @@ export default Vue.extend({
|
|||||||
default: 'timeline'
|
default: 'timeline'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
keymap(): any {
|
||||||
|
return {
|
||||||
|
't': this.focus
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
document.title = (this as any).os.instanceName;
|
document.title = (this as any).os.instanceName;
|
||||||
|
|
||||||
@ -23,6 +30,9 @@ export default Vue.extend({
|
|||||||
methods: {
|
methods: {
|
||||||
loaded() {
|
loaded() {
|
||||||
Progress.done();
|
Progress.done();
|
||||||
|
},
|
||||||
|
focus() {
|
||||||
|
this.$refs.home.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -8,6 +8,7 @@ import VueRouter from 'vue-router';
|
|||||||
import * as TreeView from 'vue-json-tree-view';
|
import * as TreeView from 'vue-json-tree-view';
|
||||||
import VAnimateCss from 'v-animate-css';
|
import VAnimateCss from 'v-animate-css';
|
||||||
import VModal from 'vue-js-modal';
|
import VModal from 'vue-js-modal';
|
||||||
|
import VueHotkey from './common/hotkey';
|
||||||
|
|
||||||
import App from './app.vue';
|
import App from './app.vue';
|
||||||
import checkForUpdate from './common/scripts/check-for-update';
|
import checkForUpdate from './common/scripts/check-for-update';
|
||||||
@ -19,6 +20,7 @@ Vue.use(VueRouter);
|
|||||||
Vue.use(TreeView);
|
Vue.use(TreeView);
|
||||||
Vue.use(VAnimateCss);
|
Vue.use(VAnimateCss);
|
||||||
Vue.use(VModal);
|
Vue.use(VModal);
|
||||||
|
Vue.use(VueHotkey);
|
||||||
|
|
||||||
// Register global directives
|
// Register global directives
|
||||||
require('./common/views/directives');
|
require('./common/views/directives');
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
|
||||||
|
|
||||||
const fetchLimit = 10;
|
const fetchLimit = 10;
|
||||||
|
|
||||||
@ -21,6 +22,9 @@ export default Vue.extend({
|
|||||||
src: {
|
src: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
tagTl: {
|
||||||
|
required: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -29,10 +33,18 @@ export default Vue.extend({
|
|||||||
fetching: true,
|
fetching: true,
|
||||||
moreFetching: false,
|
moreFetching: false,
|
||||||
existMore: false,
|
existMore: false,
|
||||||
|
streamManager: null,
|
||||||
connection: null,
|
connection: null,
|
||||||
connectionId: null,
|
connectionId: null,
|
||||||
unreadCount: 0,
|
unreadCount: 0,
|
||||||
date: null
|
date: null,
|
||||||
|
baseQuery: {
|
||||||
|
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||||
|
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||||
|
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
endpoint: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -41,51 +53,109 @@ export default Vue.extend({
|
|||||||
return this.$store.state.i.followingCount == 0;
|
return this.$store.state.i.followingCount == 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
stream(): any {
|
|
||||||
switch (this.src) {
|
|
||||||
case 'home': return (this as any).os.stream;
|
|
||||||
case 'local': return (this as any).os.streams.localTimelineStream;
|
|
||||||
case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
|
|
||||||
case 'global': return (this as any).os.streams.globalTimelineStream;
|
|
||||||
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 {
|
canFetchMore(): boolean {
|
||||||
return !this.moreFetching && !this.fetching && this.existMore;
|
return !this.moreFetching && !this.fetching && this.existMore;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.connection = this.stream.getConnection();
|
const prepend = note => {
|
||||||
this.connectionId = this.stream.use();
|
(this.$refs.timeline as any).prepend(note);
|
||||||
|
};
|
||||||
|
|
||||||
this.connection.on(this.src == 'mentions' ? 'mention' : 'note', this.onNote);
|
if (this.src == 'tag') {
|
||||||
if (this.src == 'home') {
|
this.endpoint = 'notes/search_by_tag';
|
||||||
this.connection.on('follow', this.onChangeFollowing);
|
this.query = {
|
||||||
this.connection.on('unfollow', this.onChangeFollowing);
|
query: this.tagTl.query
|
||||||
|
};
|
||||||
|
this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
|
||||||
|
this.connection.on('note', prepend);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('note', prepend);
|
||||||
|
this.connection.close();
|
||||||
|
});
|
||||||
|
} else if (this.src == 'home') {
|
||||||
|
this.endpoint = 'notes/timeline';
|
||||||
|
const onChangeFollowing = () => {
|
||||||
|
this.fetch();
|
||||||
|
};
|
||||||
|
this.streamManager = (this as any).os.stream;
|
||||||
|
this.connection = this.streamManager.getConnection();
|
||||||
|
this.connectionId = this.streamManager.use();
|
||||||
|
this.connection.on('note', prepend);
|
||||||
|
this.connection.on('follow', onChangeFollowing);
|
||||||
|
this.connection.on('unfollow', onChangeFollowing);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('note', prepend);
|
||||||
|
this.connection.off('follow', onChangeFollowing);
|
||||||
|
this.connection.off('unfollow', onChangeFollowing);
|
||||||
|
this.streamManager.dispose(this.connectionId);
|
||||||
|
});
|
||||||
|
} else if (this.src == 'local') {
|
||||||
|
this.endpoint = 'notes/local-timeline';
|
||||||
|
this.streamManager = (this as any).os.streams.localTimelineStream;
|
||||||
|
this.connection = this.streamManager.getConnection();
|
||||||
|
this.connectionId = this.streamManager.use();
|
||||||
|
this.connection.on('note', prepend);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('note', prepend);
|
||||||
|
this.streamManager.dispose(this.connectionId);
|
||||||
|
});
|
||||||
|
} else if (this.src == 'hybrid') {
|
||||||
|
this.endpoint = 'notes/hybrid-timeline';
|
||||||
|
this.streamManager = (this as any).os.streams.hybridTimelineStream;
|
||||||
|
this.connection = this.streamManager.getConnection();
|
||||||
|
this.connectionId = this.streamManager.use();
|
||||||
|
this.connection.on('note', prepend);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('note', prepend);
|
||||||
|
this.streamManager.dispose(this.connectionId);
|
||||||
|
});
|
||||||
|
} else if (this.src == 'global') {
|
||||||
|
this.endpoint = 'notes/global-timeline';
|
||||||
|
this.streamManager = (this as any).os.streams.globalTimelineStream;
|
||||||
|
this.connection = this.streamManager.getConnection();
|
||||||
|
this.connectionId = this.streamManager.use();
|
||||||
|
this.connection.on('note', prepend);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('note', prepend);
|
||||||
|
this.streamManager.dispose(this.connectionId);
|
||||||
|
});
|
||||||
|
} else if (this.src == 'mentions') {
|
||||||
|
this.endpoint = 'notes/mentions';
|
||||||
|
this.streamManager = (this as any).os.stream;
|
||||||
|
this.connection = this.streamManager.getConnection();
|
||||||
|
this.connectionId = this.streamManager.use();
|
||||||
|
this.connection.on('mention', prepend);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('mention', prepend);
|
||||||
|
this.streamManager.dispose(this.connectionId);
|
||||||
|
});
|
||||||
|
} else if (this.src == 'messages') {
|
||||||
|
this.endpoint = 'notes/mentions';
|
||||||
|
this.query = {
|
||||||
|
visibility: 'specified'
|
||||||
|
};
|
||||||
|
const onNote = note => {
|
||||||
|
if (note.visibility == 'specified') {
|
||||||
|
prepend(note);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.streamManager = (this as any).os.stream;
|
||||||
|
this.connection = this.streamManager.getConnection();
|
||||||
|
this.connectionId = this.streamManager.use();
|
||||||
|
this.connection.on('mention', onNote);
|
||||||
|
this.$once('beforeDestroy', () => {
|
||||||
|
this.connection.off('mention', onNote);
|
||||||
|
this.streamManager.dispose(this.connectionId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fetch();
|
this.fetch();
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.connection.off(this.src == 'mentions' ? 'mention' : 'note', this.onNote);
|
this.$emit('beforeDestroy');
|
||||||
if (this.src == 'home') {
|
|
||||||
this.connection.off('follow', this.onChangeFollowing);
|
|
||||||
this.connection.off('unfollow', this.onChangeFollowing);
|
|
||||||
}
|
|
||||||
this.stream.dispose(this.connectionId);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@ -93,13 +163,10 @@ export default Vue.extend({
|
|||||||
this.fetching = true;
|
this.fetching = true;
|
||||||
|
|
||||||
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
|
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
|
||||||
(this as any).api(this.endpoint, {
|
(this as any).api(this.endpoint, Object.assign({
|
||||||
limit: fetchLimit + 1,
|
limit: fetchLimit + 1,
|
||||||
untilDate: this.date ? this.date.getTime() : undefined,
|
untilDate: this.date ? this.date.getTime() : undefined
|
||||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
}, this.baseQuery, this.query)).then(notes => {
|
||||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
|
||||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
|
||||||
}).then(notes => {
|
|
||||||
if (notes.length == fetchLimit + 1) {
|
if (notes.length == fetchLimit + 1) {
|
||||||
notes.pop();
|
notes.pop();
|
||||||
this.existMore = true;
|
this.existMore = true;
|
||||||
@ -116,13 +183,10 @@ export default Vue.extend({
|
|||||||
|
|
||||||
this.moreFetching = true;
|
this.moreFetching = true;
|
||||||
|
|
||||||
const promise = (this as any).api(this.endpoint, {
|
const promise = (this as any).api(this.endpoint, Object.assign({
|
||||||
limit: fetchLimit + 1,
|
limit: fetchLimit + 1,
|
||||||
untilId: (this.$refs.timeline as any).tail().id,
|
untilId: (this.$refs.timeline as any).tail().id
|
||||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
}, this.baseQuery, this.query));
|
||||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
|
||||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.then(notes => {
|
promise.then(notes => {
|
||||||
if (notes.length == fetchLimit + 1) {
|
if (notes.length == fetchLimit + 1) {
|
||||||
@ -137,15 +201,6 @@ export default Vue.extend({
|
|||||||
return promise;
|
return promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
onNote(note) {
|
|
||||||
// Prepend a note
|
|
||||||
(this.$refs.timeline as any).prepend(note);
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeFollowing() {
|
|
||||||
this.fetch();
|
|
||||||
},
|
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
(this.$refs.timeline as any).focus();
|
(this.$refs.timeline as any).focus();
|
||||||
},
|
},
|
||||||
|
@ -7,7 +7,9 @@
|
|||||||
<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span>
|
<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span>
|
||||||
<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span>
|
<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span>
|
||||||
<span v-if="src == 'mentions'">%fa:at%%i18n:@mentions%</span>
|
<span v-if="src == 'mentions'">%fa:at%%i18n:@mentions%</span>
|
||||||
|
<span v-if="src == 'messages'">%fa:envelope R%%i18n:@messages%</span>
|
||||||
<span v-if="src == 'list'">%fa:list%{{ list.title }}</span>
|
<span v-if="src == 'list'">%fa:list%{{ list.title }}</span>
|
||||||
|
<span v-if="src == 'tag'">%fa:hashtag%{{ tagTl.title }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span style="margin-left:8px">
|
<span style="margin-left:8px">
|
||||||
<template v-if="!showNav">%fa:angle-down%</template>
|
<template v-if="!showNav">%fa:angle-down%</template>
|
||||||
@ -22,16 +24,22 @@
|
|||||||
<main :data-darkmode="$store.state.device.darkmode">
|
<main :data-darkmode="$store.state.device.darkmode">
|
||||||
<div class="nav" v-if="showNav">
|
<div class="nav" v-if="showNav">
|
||||||
<div class="bg" @click="showNav = false"></div>
|
<div class="bg" @click="showNav = false"></div>
|
||||||
|
<div class="pointer"></div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div>
|
<div>
|
||||||
<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span>
|
<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span>
|
||||||
<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
|
<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
|
||||||
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
|
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
|
||||||
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
|
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
|
||||||
|
<div class="hr"></div>
|
||||||
<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span>
|
<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">
|
<template v-if="lists">
|
||||||
|
<div class="hr"></div>
|
||||||
<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
|
<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
<div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div>
|
||||||
|
<span v-for="tl in $store.state.settings.tagTimelines" :data-active="src == 'tag' && tagTl == tl" @click="src = 'tag'; tagTl = tl" :key="tl.id">%fa:hashtag% {{ tl.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -42,6 +50,8 @@
|
|||||||
<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
|
<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
|
||||||
<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
|
<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
|
||||||
<x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
|
<x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
|
||||||
|
<x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/>
|
||||||
|
<x-tl v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/>
|
||||||
<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
|
<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@ -63,6 +73,7 @@ export default Vue.extend({
|
|||||||
src: 'home',
|
src: 'home',
|
||||||
list: null,
|
list: null,
|
||||||
lists: null,
|
lists: null,
|
||||||
|
tagTl: null,
|
||||||
showNav: false,
|
showNav: false,
|
||||||
enableLocalTimeline: false
|
enableLocalTimeline: false
|
||||||
};
|
};
|
||||||
@ -74,9 +85,16 @@ export default Vue.extend({
|
|||||||
this.saveSrc();
|
this.saveSrc();
|
||||||
},
|
},
|
||||||
|
|
||||||
list() {
|
list(x) {
|
||||||
this.showNav = false;
|
this.showNav = false;
|
||||||
this.saveSrc();
|
this.saveSrc();
|
||||||
|
if (x != null) this.tagTl = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
tagTl(x) {
|
||||||
|
this.showNav = false;
|
||||||
|
this.saveSrc();
|
||||||
|
if (x != null) this.list = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
showNav(v) {
|
showNav(v) {
|
||||||
@ -97,6 +115,8 @@ export default Vue.extend({
|
|||||||
this.src = this.$store.state.device.tl.src;
|
this.src = this.$store.state.device.tl.src;
|
||||||
if (this.src == 'list') {
|
if (this.src == 'list') {
|
||||||
this.list = this.$store.state.device.tl.arg;
|
this.list = this.$store.state.device.tl.arg;
|
||||||
|
} else if (this.src == 'tag') {
|
||||||
|
this.tagTl = this.$store.state.device.tl.arg;
|
||||||
}
|
}
|
||||||
} else if (this.$store.state.i.followingCount == 0) {
|
} else if (this.$store.state.i.followingCount == 0) {
|
||||||
this.src = 'hybrid';
|
this.src = 'hybrid';
|
||||||
@ -121,7 +141,7 @@ export default Vue.extend({
|
|||||||
saveSrc() {
|
saveSrc() {
|
||||||
this.$store.commit('device/setTl', {
|
this.$store.commit('device/setTl', {
|
||||||
src: this.src,
|
src: this.src,
|
||||||
arg: this.list
|
arg: this.src == 'list' ? this.list : this.tagTl
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -137,6 +157,26 @@ export default Vue.extend({
|
|||||||
|
|
||||||
root(isDark)
|
root(isDark)
|
||||||
> .nav
|
> .nav
|
||||||
|
> .pointer
|
||||||
|
position fixed
|
||||||
|
z-index 10002
|
||||||
|
top 56px
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
|
||||||
|
$size = 16px
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content ""
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
top -($size * 2)
|
||||||
|
left s('calc(50% - %s)', $size)
|
||||||
|
border-top solid $size transparent
|
||||||
|
border-left solid $size transparent
|
||||||
|
border-right solid $size transparent
|
||||||
|
border-bottom solid $size isDark ? #272f3a : #fff
|
||||||
|
|
||||||
> .bg
|
> .bg
|
||||||
position fixed
|
position fixed
|
||||||
z-index 10000
|
z-index 10000
|
||||||
@ -153,28 +193,22 @@ root(isDark)
|
|||||||
left 0
|
left 0
|
||||||
right 0
|
right 0
|
||||||
width 300px
|
width 300px
|
||||||
|
max-height calc(100% - 70px)
|
||||||
margin 0 auto
|
margin 0 auto
|
||||||
|
overflow auto
|
||||||
|
-webkit-overflow-scrolling touch
|
||||||
background isDark ? #272f3a : #fff
|
background isDark ? #272f3a : #fff
|
||||||
border-radius 8px
|
border-radius 8px
|
||||||
box-shadow 0 0 16px rgba(#000, 0.1)
|
box-shadow 0 0 16px rgba(#000, 0.1)
|
||||||
|
|
||||||
$balloon-size = 16px
|
|
||||||
|
|
||||||
&:after
|
|
||||||
content ""
|
|
||||||
display block
|
|
||||||
position absolute
|
|
||||||
top -($balloon-size * 2) + 1.5px
|
|
||||||
left s('calc(50% - %s)', $balloon-size)
|
|
||||||
border-top solid $balloon-size transparent
|
|
||||||
border-left solid $balloon-size transparent
|
|
||||||
border-right solid $balloon-size transparent
|
|
||||||
border-bottom solid $balloon-size isDark ? #272f3a : #fff
|
|
||||||
|
|
||||||
> div
|
> div
|
||||||
padding 8px 0
|
padding 8px 0
|
||||||
|
|
||||||
> *
|
> .hr
|
||||||
|
margin 8px 0
|
||||||
|
border-top solid 1px isDark ? rgba(#000, 0.3) : rgba(#000, 0.1)
|
||||||
|
|
||||||
|
> *:not(.hr)
|
||||||
display block
|
display block
|
||||||
padding 8px 16px
|
padding 8px 16px
|
||||||
color isDark ? #cdd0d8 : #666
|
color isDark ? #cdd0d8 : #666
|
||||||
|
@ -10,6 +10,7 @@ const defaultSettings = {
|
|||||||
home: null,
|
home: null,
|
||||||
mobileHome: [],
|
mobileHome: [],
|
||||||
deck: null,
|
deck: null,
|
||||||
|
tagTimelines: [],
|
||||||
fetchOnScroll: true,
|
fetchOnScroll: true,
|
||||||
showMaps: true,
|
showMaps: true,
|
||||||
showPostFormOnTopOfTl: false,
|
showPostFormOnTopOfTl: false,
|
||||||
|
@ -9,9 +9,9 @@ export type TextElementHashtag = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function(text: string, i: number) {
|
export default function(text: string, i: number) {
|
||||||
if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null;
|
if (!(/^\s#[^\s\.,]+/.test(text) || (i == 0 && /^#[^\s\.,]+/.test(text)))) return null;
|
||||||
const isHead = text.startsWith('#');
|
const isHead = text.startsWith('#');
|
||||||
const hashtag = text.match(/^\s?#[^\s]+/)[0];
|
const hashtag = text.match(/^\s?#[^\s\.,]+/)[0];
|
||||||
const res: any[] = !isHead ? [{
|
const res: any[] = !isHead ? [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
content: text[0]
|
content: text[0]
|
||||||
|
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;
|
hide = false;
|
||||||
} else {
|
} else {
|
||||||
// 指定されているかどうか
|
// 指定されているかどうか
|
||||||
const specified = packedNote.visibleUserIds.some((id: mongo.ObjectID) => id.equals(meId));
|
const specified = packedNote.visibleUserIds.some((id: any) => meId.equals(id));
|
||||||
|
|
||||||
if (specified) {
|
if (specified) {
|
||||||
hide = false;
|
hide = false;
|
||||||
|
@ -1,22 +1,10 @@
|
|||||||
import { INote } from '../../../models/note';
|
import { INote } from '../../../models/note';
|
||||||
import toHtml from '../../../mfm/html';
|
import toHtml from '../../../mfm/html';
|
||||||
import parse from '../../../mfm/parse';
|
import parse from '../../../mfm/parse';
|
||||||
import config from '../../../config';
|
|
||||||
|
|
||||||
export default function(note: INote) {
|
export default function(note: INote) {
|
||||||
let html = toHtml(parse(note.text), note.mentionedRemoteUsers);
|
let html = toHtml(parse(note.text), note.mentionedRemoteUsers);
|
||||||
if (html == null) html = '';
|
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;
|
return html;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import DriveFile, { IDriveFile } from '../../../models/drive-file';
|
|||||||
import Note, { INote } from '../../../models/note';
|
import Note, { INote } from '../../../models/note';
|
||||||
import User from '../../../models/user';
|
import User from '../../../models/user';
|
||||||
import toHtml from '../misc/get-note-html';
|
import toHtml from '../misc/get-note-html';
|
||||||
|
import parseMfm from '../../../mfm/parse';
|
||||||
|
|
||||||
export default async function renderNote(note: INote, dive = true): Promise<any> {
|
export default async function renderNote(note: INote, dive = true): Promise<any> {
|
||||||
const promisedFiles: Promise<IDriveFile[]> = note.fileIds
|
const promisedFiles: Promise<IDriveFile[]> = note.fileIds
|
||||||
@ -81,13 +82,39 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
|
|||||||
|
|
||||||
const files = await promisedFiles;
|
const files = await promisedFiles;
|
||||||
|
|
||||||
|
let text = note.text;
|
||||||
|
|
||||||
|
if (note.poll != null) {
|
||||||
|
if (text == null) text = '';
|
||||||
|
const url = `${config.url}/notes/${note._id}`;
|
||||||
|
// TODO: i18n
|
||||||
|
text += `\n\n[投票を見る](${url})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.renoteId != null) {
|
||||||
|
if (text == null) text = '';
|
||||||
|
const url = `${config.url}/notes/${note.renoteId}`;
|
||||||
|
text += `\n\nRE: ${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 省略されたメンションのホストを復元する
|
||||||
|
if (text != null) {
|
||||||
|
text = parseMfm(text).map(x => {
|
||||||
|
if (x.type == 'mention' && x.host == null) {
|
||||||
|
return `${x.content}@${config.host}`;
|
||||||
|
} else {
|
||||||
|
return x.content;
|
||||||
|
}
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${config.url}/notes/${note._id}`,
|
id: `${config.url}/notes/${note._id}`,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
attributedTo,
|
attributedTo,
|
||||||
summary: note.cw,
|
summary: note.cw,
|
||||||
content: toHtml(note),
|
content: toHtml(Object.assign({}, note, { text })),
|
||||||
_misskey_content: note.text,
|
_misskey_content: text,
|
||||||
published: note.createdAt.toISOString(),
|
published: note.createdAt.toISOString(),
|
||||||
to,
|
to,
|
||||||
cc,
|
cc,
|
||||||
|
@ -27,6 +27,9 @@ export const meta = {
|
|||||||
|
|
||||||
untilId: $.type(ID).optional.note({
|
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
|
_id: -1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ps.visibility) {
|
||||||
|
query.visibility = ps.visibility;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.following) {
|
if (ps.following) {
|
||||||
const followingIds = await getFriendIds(user._id);
|
const followingIds = await getFriendIds(user._id);
|
||||||
|
|
||||||
|
@ -13,12 +13,18 @@ export const meta = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
tag: $.str.note({
|
tag: $.str.optional.note({
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'タグ'
|
'ja-JP': 'タグ'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
query: $.arr($.arr($.str)).optional.note({
|
||||||
|
desc: {
|
||||||
|
'ja-JP': 'クエリ'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
includeUserIds: $.arr($.type(ID)).optional.note({
|
includeUserIds: $.arr($.type(ID)).optional.note({
|
||||||
default: []
|
default: []
|
||||||
}),
|
}),
|
||||||
@ -59,11 +65,9 @@ export const meta = {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
withFiles: $.bool.optional.nullable.note({
|
withFiles: $.bool.optional.note({
|
||||||
default: null,
|
|
||||||
|
|
||||||
desc: {
|
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({
|
sinceDate: $.num.optional.note({
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@ -126,8 +136,14 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const q: any = {
|
const q: any = {
|
||||||
$and: [{
|
$and: [ps.tag ? {
|
||||||
tagsLower: ps.tag.toLowerCase()
|
tagsLower: ps.tag.toLowerCase()
|
||||||
|
} : {
|
||||||
|
$or: ps.query.map(tags => ({
|
||||||
|
$and: tags.map(t => ({
|
||||||
|
tagsLower: t.toLowerCase()
|
||||||
|
}))
|
||||||
|
}))
|
||||||
}],
|
}],
|
||||||
deletedAt: { $exists: false }
|
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;
|
const withFiles = ps.withFiles != null ? ps.withFiles : ps.media;
|
||||||
|
|
||||||
if (withFiles != null) {
|
|
||||||
if (withFiles) {
|
if (withFiles) {
|
||||||
push({
|
push({
|
||||||
fileIds: {
|
fileIds: { $exists: true, $ne: [] }
|
||||||
$exists: true,
|
|
||||||
$ne: null
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
push({
|
|
||||||
$or: [{
|
|
||||||
fileIds: {
|
|
||||||
$exists: false
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
fileIds: null
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.poll != null) {
|
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) {
|
if (ps.sinceDate) {
|
||||||
push({
|
push({
|
||||||
createdAt: {
|
createdAt: {
|
||||||
|
@ -3,6 +3,7 @@ import Xev from 'xev';
|
|||||||
|
|
||||||
import { IUser } from '../../../models/user';
|
import { IUser } from '../../../models/user';
|
||||||
import Mute from '../../../models/mute';
|
import Mute from '../../../models/mute';
|
||||||
|
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
|
||||||
|
|
||||||
export default async function(
|
export default async function(
|
||||||
request: websocket.request,
|
request: websocket.request,
|
||||||
@ -15,17 +16,8 @@ export default async function(
|
|||||||
|
|
||||||
// Subscribe stream
|
// Subscribe stream
|
||||||
subscriber.on('global-timeline', async note => {
|
subscriber.on('global-timeline', async note => {
|
||||||
//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (mutedUserIds.indexOf(note.userId) != -1) {
|
if (shouldMuteThisNote(note, mutedUserIds)) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
connection.send(JSON.stringify({
|
connection.send(JSON.stringify({
|
||||||
type: 'note',
|
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 readNotification from '../common/read-notification';
|
||||||
import call from '../call';
|
import call from '../call';
|
||||||
import { IApp } from '../../../models/app';
|
import { IApp } from '../../../models/app';
|
||||||
|
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
|
||||||
|
|
||||||
const log = debug('misskey');
|
const log = debug('misskey');
|
||||||
|
|
||||||
@ -45,15 +46,7 @@ export default async function(
|
|||||||
|
|
||||||
//#region 流れてきたメッセージがミュートしているユーザーが関わるものだったら無視する
|
//#region 流れてきたメッセージがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (x.type == 'note') {
|
if (x.type == 'note') {
|
||||||
if (mutedUserIds.includes(x.body.userId)) {
|
if (shouldMuteThisNote(x.body, mutedUserIds)) return;
|
||||||
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;
|
|
||||||
}
|
|
||||||
} else if (x.type == 'notification') {
|
} else if (x.type == 'notification') {
|
||||||
if (mutedUserIds.includes(x.body.userId)) {
|
if (mutedUserIds.includes(x.body.userId)) {
|
||||||
return;
|
return;
|
||||||
|
@ -4,6 +4,7 @@ import Xev from 'xev';
|
|||||||
import { IUser } from '../../../models/user';
|
import { IUser } from '../../../models/user';
|
||||||
import Mute from '../../../models/mute';
|
import Mute from '../../../models/mute';
|
||||||
import { pack } from '../../../models/note';
|
import { pack } from '../../../models/note';
|
||||||
|
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
|
||||||
|
|
||||||
export default async function(
|
export default async function(
|
||||||
request: websocket.request,
|
request: websocket.request,
|
||||||
@ -26,17 +27,8 @@ export default async function(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (mutedUserIds.indexOf(note.userId) != -1) {
|
if (shouldMuteThisNote(note, mutedUserIds)) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
connection.send(JSON.stringify({
|
connection.send(JSON.stringify({
|
||||||
type: 'note',
|
type: 'note',
|
||||||
|
@ -4,6 +4,7 @@ import Xev from 'xev';
|
|||||||
import { IUser } from '../../../models/user';
|
import { IUser } from '../../../models/user';
|
||||||
import Mute from '../../../models/mute';
|
import Mute from '../../../models/mute';
|
||||||
import { pack } from '../../../models/note';
|
import { pack } from '../../../models/note';
|
||||||
|
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
|
||||||
|
|
||||||
export default async function(
|
export default async function(
|
||||||
request: websocket.request,
|
request: websocket.request,
|
||||||
@ -23,17 +24,8 @@ export default async function(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (mutedUserIds.indexOf(note.userId) != -1) {
|
if (shouldMuteThisNote(note, mutedUserIds)) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
connection.send(JSON.stringify({
|
connection.send(JSON.stringify({
|
||||||
type: 'note',
|
type: 'note',
|
||||||
|
@ -14,6 +14,7 @@ import reversiGameStream from './stream/games/reversi-game';
|
|||||||
import reversiStream from './stream/games/reversi';
|
import reversiStream from './stream/games/reversi';
|
||||||
import serverStatsStream from './stream/server-stats';
|
import serverStatsStream from './stream/server-stats';
|
||||||
import notesStatsStream from './stream/notes-stats';
|
import notesStatsStream from './stream/notes-stats';
|
||||||
|
import hashtagStream from './stream/hashtag';
|
||||||
import { ParsedUrlQuery } from 'querystring';
|
import { ParsedUrlQuery } from 'querystring';
|
||||||
import authenticate from './authenticate';
|
import authenticate from './authenticate';
|
||||||
|
|
||||||
@ -44,6 +45,12 @@ module.exports = (server: http.Server) => {
|
|||||||
ev.removeAllListeners();
|
ev.removeAllListeners();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connection.on('message', async (data) => {
|
||||||
|
if (data.utf8Data == 'ping') {
|
||||||
|
connection.send('pong');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const q = request.resourceURL.query as ParsedUrlQuery;
|
const q = request.resourceURL.query as ParsedUrlQuery;
|
||||||
const [user, app] = await authenticate(q.i as string);
|
const [user, app] = await authenticate(q.i as string);
|
||||||
|
|
||||||
@ -57,6 +64,11 @@ module.exports = (server: http.Server) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.resourceURL.pathname === '/hashtag') {
|
||||||
|
hashtagStream(request, connection, ev, user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
connection.send('authentication-failed');
|
connection.send('authentication-failed');
|
||||||
connection.close();
|
connection.close();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import es from '../../db/elasticsearch';
|
import es from '../../db/elasticsearch';
|
||||||
import Note, { pack, INote } from '../../models/note';
|
import Note, { pack, INote } from '../../models/note';
|
||||||
import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user';
|
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 Following from '../../models/following';
|
||||||
import { deliver } from '../../queue';
|
import { deliver } from '../../queue';
|
||||||
import renderNote from '../../remote/activitypub/renderer/note';
|
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 }));
|
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);
|
const note = await insertNote(user, data, tags, mentionedUsers);
|
||||||
|
|
||||||
res(note);
|
res(note);
|
||||||
@ -181,10 +189,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
|
|||||||
noteObj.isFirstNote = true;
|
noteObj.isFirstNote = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tags.length > 0) {
|
||||||
|
publishHashtagStream(noteObj);
|
||||||
|
}
|
||||||
|
|
||||||
const nm = new NotificationManager(user, note);
|
const nm = new NotificationManager(user, note);
|
||||||
const nmRelatedPromises = [];
|
const nmRelatedPromises = [];
|
||||||
|
|
||||||
createMentionedEvents(mentionedUsers, noteObj, nm);
|
createMentionedEvents(mentionedUsers, note, nm);
|
||||||
|
|
||||||
const noteActivity = await renderActivity(data, note);
|
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)) {
|
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, {
|
const detailPackedNote = await pack(note, null, {
|
||||||
detail: true,
|
detail: true,
|
||||||
skipHide: 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) => {
|
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
|
// Create notification
|
||||||
nm.push(u._id, 'mention');
|
nm.push(u._id, 'mention');
|
||||||
|
@ -78,6 +78,10 @@ class Publisher {
|
|||||||
public publishGlobalTimelineStream = (note: any): void => {
|
public publishGlobalTimelineStream = (note: any): void => {
|
||||||
this.publish('global-timeline', null, note);
|
this.publish('global-timeline', null, note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public publishHashtagStream = (note: any): void => {
|
||||||
|
this.publish('hashtag', null, note);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const publisher = new Publisher();
|
const publisher = new Publisher();
|
||||||
@ -95,3 +99,4 @@ export const publishReversiGameStream = publisher.publishReversiGameStream;
|
|||||||
export const publishLocalTimelineStream = publisher.publishLocalTimelineStream;
|
export const publishLocalTimelineStream = publisher.publishLocalTimelineStream;
|
||||||
export const publishHybridTimelineStream = publisher.publishHybridTimelineStream;
|
export const publishHybridTimelineStream = publisher.publishHybridTimelineStream;
|
||||||
export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
|
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', () => {
|
it('hashtag', () => {
|
||||||
const tokens = analyze('Strawberry Pasta #alice');
|
const tokens1 = analyze('Strawberry Pasta #alice');
|
||||||
assert.deepEqual([
|
assert.deepEqual([
|
||||||
{ type: 'text', content: 'Strawberry Pasta ' },
|
{ type: 'text', content: 'Strawberry Pasta ' },
|
||||||
{ type: 'hashtag', content: '#alice', hashtag: 'alice' }
|
{ 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', () => {
|
it('url', () => {
|
||||||
|
Reference in New Issue
Block a user