Compare commits
83 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
30608d3e22 | ||
![]() |
8bf4e55338 | ||
![]() |
6ead1de383 | ||
![]() |
3b628ec3c4 | ||
![]() |
0ed704d173 | ||
![]() |
87b6ef0ec5 | ||
![]() |
5184a07cf2 | ||
![]() |
dba04cc59c | ||
![]() |
f4045fb5b3 | ||
![]() |
16c36163b4 | ||
![]() |
1ac033ff18 | ||
![]() |
ccfd48232a | ||
![]() |
429bf179dc | ||
![]() |
8ba3fb13eb | ||
![]() |
11496d887e | ||
![]() |
bec48319ec | ||
![]() |
71a93b2b43 | ||
![]() |
6ed3f9e414 | ||
![]() |
dc8f592c1f | ||
![]() |
f66c31c771 | ||
![]() |
55e2ae1408 | ||
![]() |
19c72627fc | ||
![]() |
2a4c53c3a4 | ||
![]() |
1f2ebce8ed | ||
![]() |
fcea9dacb7 | ||
![]() |
908872f374 | ||
![]() |
f688ceafb8 | ||
![]() |
b47b5d6d8b | ||
![]() |
31ce3aa312 | ||
![]() |
5b22d92e99 | ||
![]() |
df148e25da | ||
![]() |
4b26df5c3a | ||
![]() |
e765be4205 | ||
![]() |
f7d2457063 | ||
![]() |
6032d803aa | ||
![]() |
0de371db38 | ||
![]() |
ce3797c4af | ||
![]() |
56dd8c298b | ||
![]() |
3533257efe | ||
![]() |
dc2f08721d | ||
![]() |
66608a4131 | ||
![]() |
2fa90131eb | ||
![]() |
a51ed28db6 | ||
![]() |
5ec290663b | ||
![]() |
1374d6e34d | ||
![]() |
45ade17c58 | ||
![]() |
c753e26187 | ||
![]() |
577929eed1 | ||
![]() |
1fde8a8fb0 | ||
![]() |
77e53cbf9e | ||
![]() |
ab83e08bc7 | ||
![]() |
2fad6e6d5f | ||
![]() |
a3604a6c95 | ||
![]() |
44f6fe6f1f | ||
![]() |
311b4e90ca | ||
![]() |
f5a937c523 | ||
![]() |
0632a3ed3f | ||
![]() |
71bada97df | ||
![]() |
62509edcbe | ||
![]() |
f97cdfaa20 | ||
![]() |
67ec10e86d | ||
![]() |
481b3f2c58 | ||
![]() |
7d599a68ea | ||
![]() |
7ccff732b8 | ||
![]() |
7587c896d5 | ||
![]() |
91297f1ab3 | ||
![]() |
d872a16fe0 | ||
![]() |
60aa35adf8 | ||
![]() |
5035b66773 | ||
![]() |
fa9da8ecab | ||
![]() |
1f9bca7188 | ||
![]() |
ffa5bdeb50 | ||
![]() |
e6bfb7398e | ||
![]() |
6def0c776f | ||
![]() |
24bae9eaed | ||
![]() |
fb5175a283 | ||
![]() |
6e49437154 | ||
![]() |
2511ed56ac | ||
![]() |
c4bfc99cf5 | ||
![]() |
4efe38440d | ||
![]() |
4a5f2c3c40 | ||
![]() |
109738ccb9 | ||
![]() |
25438c4d64 |
@@ -78,7 +78,7 @@ gulp.task('build:copy', ['build:copy:views', 'build:copy:lang'], () =>
|
|||||||
]).pipe(gulp.dest('./built/'))
|
]).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.1",
|
"version": "8.53.0",
|
||||||
"clientVersion": "1.0.9813",
|
"clientVersion": "1.0.9898",
|
||||||
"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",
|
||||||
|
109
src/client/app/common/hotkey.ts
Normal file
109
src/client/app/common/hotkey.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import keyCode from './keycode';
|
||||||
|
import { concat } from '../../../prelude/array';
|
||||||
|
|
||||||
|
type pattern = {
|
||||||
|
which: string[];
|
||||||
|
ctrl?: boolean;
|
||||||
|
shift?: boolean;
|
||||||
|
alt?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type action = {
|
||||||
|
patterns: pattern[];
|
||||||
|
|
||||||
|
callback: Function;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => {
|
||||||
|
const result = {
|
||||||
|
patterns: [],
|
||||||
|
callback: callback
|
||||||
|
} as action;
|
||||||
|
|
||||||
|
result.patterns = patterns.split('|').map(part => {
|
||||||
|
const pattern = {
|
||||||
|
which: [],
|
||||||
|
ctrl: false,
|
||||||
|
alt: false,
|
||||||
|
shift: false
|
||||||
|
} as pattern;
|
||||||
|
|
||||||
|
part.trim().split('+').forEach(key => {
|
||||||
|
key = key.trim().toLowerCase();
|
||||||
|
switch (key) {
|
||||||
|
case 'ctrl': pattern.ctrl = true; break;
|
||||||
|
case 'alt': pattern.alt = true; break;
|
||||||
|
case 'shift': pattern.shift = true; break;
|
||||||
|
default: pattern.which = keyCode(key).map(k => k.toLowerCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return pattern;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ignoreElemens = ['input', 'textarea'];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(Vue) {
|
||||||
|
Vue.directive('hotkey', {
|
||||||
|
bind(el, binding) {
|
||||||
|
el._hotkey_global = binding.modifiers.global === true;
|
||||||
|
|
||||||
|
const actions = getKeyMap(binding.value);
|
||||||
|
|
||||||
|
// flatten
|
||||||
|
const reservedKeys = concat(concat(actions.map(a => a.patterns.map(p => p.which))));
|
||||||
|
|
||||||
|
el.dataset.reservedKeys = reservedKeys.map(key => `'${key}'`).join(' ');
|
||||||
|
|
||||||
|
el._keyHandler = e => {
|
||||||
|
const key = e.code.toLowerCase();
|
||||||
|
|
||||||
|
const targetReservedKeys = document.activeElement ? ((document.activeElement as any).dataset || {}).reservedKeys || '' : '';
|
||||||
|
if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
|
||||||
|
|
||||||
|
for (const action of actions) {
|
||||||
|
if (el._hotkey_global && targetReservedKeys.includes(`'${key}'`)) break;
|
||||||
|
|
||||||
|
const matched = action.patterns.some(pattern => {
|
||||||
|
const matched = pattern.which.includes(key) &&
|
||||||
|
pattern.ctrl == e.ctrlKey &&
|
||||||
|
pattern.shift == e.shiftKey &&
|
||||||
|
pattern.alt == e.altKey;
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
action.callback(e);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (el._hotkey_global) {
|
||||||
|
document.addEventListener('keydown', el._keyHandler);
|
||||||
|
} else {
|
||||||
|
el.addEventListener('keydown', el._keyHandler);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unbind(el) {
|
||||||
|
if (el._hotkey_global) {
|
||||||
|
document.removeEventListener('keydown', el._keyHandler);
|
||||||
|
} else {
|
||||||
|
el.removeEventListener('keydown', el._keyHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
33
src/client/app/common/keycode.ts
Normal file
33
src/client/app/common/keycode.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export default (input: string): string[] => {
|
||||||
|
if (Object.keys(aliases).some(a => a.toLowerCase() == input.toLowerCase())) {
|
||||||
|
const codes = aliases[input];
|
||||||
|
return Array.isArray(codes) ? codes : [codes];
|
||||||
|
} else {
|
||||||
|
return [input];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const aliases = {
|
||||||
|
'esc': 'Escape',
|
||||||
|
'enter': ['Enter', 'NumpadEnter'],
|
||||||
|
'up': 'ArrowUp',
|
||||||
|
'down': 'ArrowDown',
|
||||||
|
'left': 'ArrowLeft',
|
||||||
|
'right': 'ArrowRight',
|
||||||
|
'plus': ['NumpadAdd', 'Semicolon'],
|
||||||
|
};
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Programatically add the following
|
||||||
|
*/
|
||||||
|
|
||||||
|
// lower case chars
|
||||||
|
for (let i = 97; i < 123; i++) {
|
||||||
|
const char = String.fromCharCode(i);
|
||||||
|
aliases[char] = `Key${char.toUpperCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// numbers
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
aliases[i] = [`Numpad${i}`, `Digit${i}`];
|
||||||
|
}
|
13
src/client/app/common/scripts/streaming/hashtag.ts
Normal file
13
src/client/app/common/scripts/streaming/hashtag.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import Stream from './stream';
|
||||||
|
import MiOS from '../../../mios';
|
||||||
|
|
||||||
|
export class HashtagStream extends Stream {
|
||||||
|
constructor(os: MiOS, me, q) {
|
||||||
|
super(os, 'hashtag', me ? {
|
||||||
|
i: me.token,
|
||||||
|
q: JSON.stringify(q)
|
||||||
|
} : {
|
||||||
|
q: JSON.stringify(q)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -2,9 +2,9 @@
|
|||||||
<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv">
|
<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv">
|
||||||
<div class="backdrop" ref="backdrop" @click="close"></div>
|
<div class="backdrop" ref="backdrop" @click="close"></div>
|
||||||
<div class="popover" :class="{ hukidasi }" ref="popover">
|
<div class="popover" :class="{ hukidasi }" ref="popover">
|
||||||
<template v-for="item in items">
|
<template v-for="item, i in items">
|
||||||
<div v-if="item === null"></div>
|
<div v-if="item === null"></div>
|
||||||
<button v-if="item" @click="clicked(item.action)" v-html="item.icon ? item.icon + ' ' + item.text : item.text"></button>
|
<button v-if="item" @click="clicked(item.action)" v-html="item.icon ? item.icon + ' ' + item.text : item.text" :tabindex="i"></button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mk-reaction-picker">
|
<div class="mk-reaction-picker" v-hotkey.global="keymap">
|
||||||
<div class="backdrop" ref="backdrop" @click="close"></div>
|
<div class="backdrop" ref="backdrop" @click="close"></div>
|
||||||
<div class="popover" :class="{ compact, big }" ref="popover">
|
<div class="popover" :class="{ compact, big }" ref="popover">
|
||||||
<p v-if="!compact">{{ title }}</p>
|
<p v-if="!compact">{{ title }}</p>
|
||||||
<div>
|
<div ref="buttons" :class="{ showFocus }">
|
||||||
<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
|
<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
|
||||||
<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button>
|
<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button>
|
||||||
<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button>
|
<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button>
|
||||||
@@ -31,30 +31,84 @@ export default Vue.extend({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
|
||||||
source: {
|
source: {
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
|
||||||
compact: {
|
compact: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
cb: {
|
cb: {
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
|
||||||
big: {
|
big: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
|
||||||
|
showFocus: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
|
||||||
|
animation: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
title: placeholder
|
title: placeholder,
|
||||||
|
focus: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
keymap(): any {
|
||||||
|
return {
|
||||||
|
'esc': this.close,
|
||||||
|
'enter|space|plus': this.choose,
|
||||||
|
'up|k': this.focusUp,
|
||||||
|
'left|h|shift+tab': this.focusLeft,
|
||||||
|
'right|l|tab': this.focusRight,
|
||||||
|
'down|j': this.focusDown,
|
||||||
|
'1': () => this.react('like'),
|
||||||
|
'2': () => this.react('love'),
|
||||||
|
'3': () => this.react('laugh'),
|
||||||
|
'4': () => this.react('hmm'),
|
||||||
|
'5': () => this.react('surprise'),
|
||||||
|
'6': () => this.react('congrats'),
|
||||||
|
'7': () => this.react('angry'),
|
||||||
|
'8': () => this.react('confused'),
|
||||||
|
'9': () => this.react('rip'),
|
||||||
|
'0': () => this.react('pudding'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
focus(i) {
|
||||||
|
this.$refs.buttons.children[i].focus();
|
||||||
|
|
||||||
|
if (this.showFocus) {
|
||||||
|
this.title = this.$refs.buttons.children[i].title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
this.focus = 0;
|
||||||
|
|
||||||
const popover = this.$refs.popover as any;
|
const popover = this.$refs.popover as any;
|
||||||
|
|
||||||
const rect = this.source.getBoundingClientRect();
|
const rect = this.source.getBoundingClientRect();
|
||||||
@@ -76,7 +130,7 @@ export default Vue.extend({
|
|||||||
anime({
|
anime({
|
||||||
targets: this.$refs.backdrop,
|
targets: this.$refs.backdrop,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
duration: 100,
|
duration: this.animation ? 100 : 0,
|
||||||
easing: 'linear'
|
easing: 'linear'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,10 +138,11 @@ export default Vue.extend({
|
|||||||
targets: this.$refs.popover,
|
targets: this.$refs.popover,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
scale: [0.5, 1],
|
scale: [0.5, 1],
|
||||||
duration: 500
|
duration: this.animation ? 500 : 0
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
react(reaction) {
|
react(reaction) {
|
||||||
(this as any).api('notes/reactions/create', {
|
(this as any).api('notes/reactions/create', {
|
||||||
@@ -95,21 +150,25 @@ export default Vue.extend({
|
|||||||
reaction: reaction
|
reaction: reaction
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
if (this.cb) this.cb();
|
if (this.cb) this.cb();
|
||||||
|
this.$emit('closed');
|
||||||
this.destroyDom();
|
this.destroyDom();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseover(e) {
|
onMouseover(e) {
|
||||||
this.title = e.target.title;
|
this.title = e.target.title;
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseout(e) {
|
onMouseout(e) {
|
||||||
this.title = placeholder;
|
this.title = placeholder;
|
||||||
},
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
(this.$refs.backdrop as any).style.pointerEvents = 'none';
|
(this.$refs.backdrop as any).style.pointerEvents = 'none';
|
||||||
anime({
|
anime({
|
||||||
targets: this.$refs.backdrop,
|
targets: this.$refs.backdrop,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
duration: 200,
|
duration: this.animation ? 200 : 0,
|
||||||
easing: 'linear'
|
easing: 'linear'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,10 +177,33 @@ export default Vue.extend({
|
|||||||
targets: this.$refs.popover,
|
targets: this.$refs.popover,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
scale: 0.5,
|
scale: 0.5,
|
||||||
duration: 200,
|
duration: this.animation ? 200 : 0,
|
||||||
easing: 'easeInBack',
|
easing: 'easeInBack',
|
||||||
complete: () => this.destroyDom()
|
complete: () => {
|
||||||
|
this.$emit('closed');
|
||||||
|
this.destroyDom();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
focusUp() {
|
||||||
|
this.focus = this.focus == 0 ? 9 : this.focus < 5 ? (this.focus + 4) : (this.focus - 5);
|
||||||
|
},
|
||||||
|
|
||||||
|
focusDown() {
|
||||||
|
this.focus = this.focus == 9 ? 0 : this.focus >= 5 ? (this.focus - 4) : (this.focus + 5);
|
||||||
|
},
|
||||||
|
|
||||||
|
focusRight() {
|
||||||
|
this.focus = this.focus == 9 ? 0 : (this.focus + 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
focusLeft() {
|
||||||
|
this.focus = this.focus == 0 ? 9 : (this.focus - 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
choose() {
|
||||||
|
this.$refs.buttons.childNodes[this.focus].click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -207,6 +289,21 @@ root(isDark)
|
|||||||
width 240px
|
width 240px
|
||||||
text-align center
|
text-align center
|
||||||
|
|
||||||
|
&.showFocus
|
||||||
|
> button:focus
|
||||||
|
z-index 1
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content ""
|
||||||
|
pointer-events none
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
right 0
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
border 2px solid rgba($theme-color, 0.3)
|
||||||
|
border-radius 4px
|
||||||
|
|
||||||
> button
|
> button
|
||||||
padding 0
|
padding 0
|
||||||
width 40px
|
width 40px
|
||||||
|
@@ -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>
|
||||||
@@ -40,18 +40,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
|
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
|
||||||
<button class="replyButton" @click="reply" title="%i18n:@reply%">
|
<button class="replyButton" @click="reply()" title="%i18n:@reply%">
|
||||||
<template v-if="p.reply">%fa:reply-all%</template>
|
<template v-if="p.reply">%fa:reply-all%</template>
|
||||||
<template v-else>%fa:reply%</template>
|
<template v-else>%fa:reply%</template>
|
||||||
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
|
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button class="renoteButton" @click="renote" title="%i18n:@renote%">
|
<button class="renoteButton" @click="renote()" title="%i18n:@renote%">
|
||||||
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
|
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@add-reaction%">
|
<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react()" ref="reactButton" title="%i18n:@add-reaction%">
|
||||||
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
|
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button @click="menu" ref="menuButton">
|
<button @click="menu()" ref="menuButton">
|
||||||
%fa:ellipsis-h%
|
%fa:ellipsis-h%
|
||||||
</button>
|
</button>
|
||||||
<!-- <button title="%i18n:@detail">
|
<!-- <button title="%i18n:@detail">
|
||||||
@@ -111,6 +111,28 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
keymap(): any {
|
||||||
|
return {
|
||||||
|
'r|left': () => this.reply(true),
|
||||||
|
'e|a|plus': () => this.react(true),
|
||||||
|
'q|right': () => this.renote(true),
|
||||||
|
'ctrl+q|ctrl+right': this.renoteDirectly,
|
||||||
|
'up|k|shift+tab': this.focusBefore,
|
||||||
|
'down|j|tab': this.focusAfter,
|
||||||
|
'm|o': () => this.menu(true),
|
||||||
|
'1': () => this.reactDirectly('like'),
|
||||||
|
'2': () => this.reactDirectly('love'),
|
||||||
|
'3': () => this.reactDirectly('laugh'),
|
||||||
|
'4': () => this.reactDirectly('hmm'),
|
||||||
|
'5': () => this.reactDirectly('surprise'),
|
||||||
|
'6': () => this.reactDirectly('congrats'),
|
||||||
|
'7': () => this.reactDirectly('angry'),
|
||||||
|
'8': () => this.reactDirectly('confused'),
|
||||||
|
'9': () => this.reactDirectly('rip'),
|
||||||
|
'0': () => this.reactDirectly('pudding'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
isRenote(): boolean {
|
isRenote(): boolean {
|
||||||
return (this.note.renote &&
|
return (this.note.renote &&
|
||||||
this.note.text == null &&
|
this.note.text == null &&
|
||||||
@@ -220,67 +242,65 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
reply() {
|
reply(viaKeyboard = false) {
|
||||||
(this as any).os.new(MkPostFormWindow, {
|
(this as any).os.new(MkPostFormWindow, {
|
||||||
reply: this.p
|
reply: this.p,
|
||||||
});
|
animation: !viaKeyboard
|
||||||
|
}).$once('closed', this.focus);
|
||||||
},
|
},
|
||||||
|
|
||||||
renote() {
|
renote(viaKeyboard = false) {
|
||||||
(this as any).os.new(MkRenoteFormWindow, {
|
(this as any).os.new(MkRenoteFormWindow, {
|
||||||
note: this.p
|
note: this.p,
|
||||||
|
animation: !viaKeyboard
|
||||||
|
}).$once('closed', this.focus);
|
||||||
|
},
|
||||||
|
|
||||||
|
renoteDirectly() {
|
||||||
|
(this as any).api('notes/create', {
|
||||||
|
renoteId: this.p.id
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
react() {
|
react(viaKeyboard = false) {
|
||||||
|
this.blur();
|
||||||
(this as any).os.new(MkReactionPicker, {
|
(this as any).os.new(MkReactionPicker, {
|
||||||
source: this.$refs.reactButton,
|
source: this.$refs.reactButton,
|
||||||
note: this.p
|
note: this.p,
|
||||||
|
showFocus: viaKeyboard,
|
||||||
|
animation: !viaKeyboard
|
||||||
|
}).$once('closed', this.focus);
|
||||||
|
},
|
||||||
|
|
||||||
|
reactDirectly(reaction) {
|
||||||
|
(this as any).api('notes/reactions/create', {
|
||||||
|
noteId: this.p.id,
|
||||||
|
reaction: reaction
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
menu() {
|
menu(viaKeyboard = false) {
|
||||||
(this as any).os.new(MkNoteMenu, {
|
(this as any).os.new(MkNoteMenu, {
|
||||||
source: this.$refs.menuButton,
|
source: this.$refs.menuButton,
|
||||||
note: this.p
|
note: this.p,
|
||||||
});
|
animation: !viaKeyboard
|
||||||
|
}).$once('closed', this.focus);
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeydown(e) {
|
focus() {
|
||||||
let shouldBeCancel = true;
|
this.$el.focus();
|
||||||
|
},
|
||||||
|
|
||||||
switch (true) {
|
blur() {
|
||||||
case e.which == 38: // [↑]
|
this.$el.blur();
|
||||||
case e.which == 74: // [j]
|
},
|
||||||
case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
|
|
||||||
|
focusBefore() {
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -10,9 +10,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||||
<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" ref="notes">
|
||||||
<template v-for="(note, i) in _notes">
|
<template v-for="(note, i) in _notes">
|
||||||
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
|
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" ref="note"/>
|
||||||
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
||||||
<span>%fa:angle-up%{{ note._datetext }}</span>
|
<span>%fa:angle-up%{{ note._datetext }}</span>
|
||||||
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
|
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
|
||||||
@@ -89,7 +89,7 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
(this.$el as any).children[0].focus();
|
(this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
onNoteUpdated(i, note) {
|
onNoteUpdated(i, note) {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window class="mk-post-form-window" ref="window" is-modal @closed="$destroy">
|
<mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed" :animation="animation">
|
||||||
<span slot="header" class="mk-post-form-window--header">
|
<span slot="header" class="mk-post-form-window--header">
|
||||||
<span class="icon" v-if="geo">%fa:map-marker-alt%</span>
|
<span class="icon" v-if="geo">%fa:map-marker-alt%</span>
|
||||||
<span v-if="!reply">%i18n:@note%</span>
|
<span v-if="!reply">%i18n:@note%</span>
|
||||||
@@ -25,7 +25,19 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: ['reply'],
|
props: {
|
||||||
|
reply: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
animation: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
uploadings: [],
|
uploadings: [],
|
||||||
@@ -33,11 +45,13 @@ export default Vue.extend({
|
|||||||
geo: null
|
geo: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
(this.$refs.form as any).focus();
|
(this.$refs.form as any).focus();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onChangeUploadings(files) {
|
onChangeUploadings(files) {
|
||||||
this.uploadings = files;
|
this.uploadings = files;
|
||||||
@@ -53,6 +67,10 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
onPosted() {
|
onPosted() {
|
||||||
(this.$refs.window as any).close();
|
(this.$refs.window as any).close();
|
||||||
|
},
|
||||||
|
onWindowClosed() {
|
||||||
|
this.$emit('closed');
|
||||||
|
this.destroyDom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -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" :animation="animation">
|
||||||
<span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span>
|
<span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span>
|
||||||
<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/>
|
<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/>
|
||||||
</mk-window>
|
</mk-window>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -9,26 +9,48 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: ['note'],
|
props: {
|
||||||
mounted() {
|
note: {
|
||||||
document.addEventListener('keydown', this.onDocumentKeydown);
|
type: Object,
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
|
||||||
document.removeEventListener('keydown', this.onDocumentKeydown);
|
animation: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
keymap(): any {
|
||||||
|
return {
|
||||||
|
'esc': this.close,
|
||||||
|
'enter': this.post,
|
||||||
|
'q': this.quote,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onDocumentKeydown(e) {
|
post() {
|
||||||
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
|
(this.$refs.form as any).ok();
|
||||||
if (e.which == 27) { // Esc
|
},
|
||||||
|
quote() {
|
||||||
|
(this.$refs.form as any).onQuote();
|
||||||
|
},
|
||||||
|
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">
|
||||||
|
@@ -76,6 +76,11 @@ export default Vue.extend({
|
|||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -142,7 +147,7 @@ export default Vue.extend({
|
|||||||
anime({
|
anime({
|
||||||
targets: bg,
|
targets: bg,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
duration: 100,
|
duration: this.animation ? 100 : 0,
|
||||||
easing: 'linear'
|
easing: 'linear'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -152,7 +157,7 @@ export default Vue.extend({
|
|||||||
targets: main,
|
targets: main,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
scale: [1.1, 1],
|
scale: [1.1, 1],
|
||||||
duration: 200,
|
duration: this.animation ? 200 : 0,
|
||||||
easing: 'easeOutQuad'
|
easing: 'easeOutQuad'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,7 +165,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$emit('opened');
|
this.$emit('opened');
|
||||||
}, 300);
|
}, this.animation ? 300 : 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
@@ -174,7 +179,7 @@ export default Vue.extend({
|
|||||||
anime({
|
anime({
|
||||||
targets: bg,
|
targets: bg,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
duration: 300,
|
duration: this.animation ? 300 : 0,
|
||||||
easing: 'linear'
|
easing: 'linear'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -185,14 +190,14 @@ export default Vue.extend({
|
|||||||
targets: main,
|
targets: main,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
scale: 0.8,
|
scale: 0.8,
|
||||||
duration: 300,
|
duration: this.animation ? 300 : 0,
|
||||||
easing: [0.5, -0.5, 1, 0.5]
|
easing: [0.5, -0.5, 1, 0.5]
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.destroyDom();
|
|
||||||
this.$emit('closed');
|
this.$emit('closed');
|
||||||
}, 300);
|
this.destroyDom();
|
||||||
|
}, this.animation ? 300 : 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
popout() {
|
popout() {
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
<main>
|
<main>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<x-header :user="user"/>
|
<x-header :user="user"/>
|
||||||
<mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/>
|
<mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
|
||||||
<x-timeline class="timeline" ref="tl" :user="user"/>
|
<x-timeline class="timeline" ref="tl" :user="user"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="side">
|
<div class="side">
|
||||||
@@ -28,7 +28,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import parseAcct from '../../../../../../misc/acct/parse';
|
import parseAcct from '../../../../../../misc/acct/parse';
|
||||||
import getUserName from '../../../../../../misc/get-user-name';
|
|
||||||
import Progress from '../../../../common/scripts/loading';
|
import Progress from '../../../../common/scripts/loading';
|
||||||
import XHeader from './user.header.vue';
|
import XHeader from './user.header.vue';
|
||||||
import XTimeline from './user.timeline.vue';
|
import XTimeline from './user.timeline.vue';
|
||||||
|
@@ -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" v-if="lists.length > 0"></div>
|
||||||
<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
|
<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
<div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div>
|
||||||
|
<span v-for="tl in $store.state.settings.tagTimelines" :data-active="src == 'tag' && tagTl == tl" @click="src = 'tag'; tagTl = tl" :key="tl.id">%fa:hashtag% {{ tl.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="root home">
|
<div class="root home">
|
||||||
<mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/>
|
<mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
|
||||||
<section class="recent-notes">
|
<section class="recent-notes">
|
||||||
<h2>%fa:R comments%%i18n:@recent-notes%</h2>
|
<h2>%fa:R comments%%i18n:@recent-notes%</h2>
|
||||||
<div>
|
<div>
|
||||||
|
@@ -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,
|
||||||
|
@@ -101,15 +101,15 @@ props:
|
|||||||
ja-JP: "投稿の数"
|
ja-JP: "投稿の数"
|
||||||
en-US: "The number of the notes of this user"
|
en-US: "The number of the notes of this user"
|
||||||
|
|
||||||
pinnedNote:
|
pinnedNotes:
|
||||||
type: "entity(Note)"
|
type: "entity(Note)[]"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja-JP: "ピン留めされた投稿"
|
ja-JP: "ピン留めされた投稿"
|
||||||
en-US: "The pinned note of this user"
|
en-US: "The pinned note of this user"
|
||||||
|
|
||||||
pinnedNoteId:
|
pinnedNoteIds:
|
||||||
type: "id(Note)"
|
type: "id(Note)[]"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja-JP: "ピン留めされた投稿のID"
|
ja-JP: "ピン留めされた投稿のID"
|
||||||
|
@@ -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;
|
||||||
|
@@ -35,6 +35,28 @@ User.createIndex('uri', { sparse: true, unique: true });
|
|||||||
|
|
||||||
export default User;
|
export default User;
|
||||||
|
|
||||||
|
// 後方互換性のため
|
||||||
|
User.findOne({
|
||||||
|
pinnedNoteId: { $exists: true }
|
||||||
|
}).then(async x => {
|
||||||
|
if (x == null) return;
|
||||||
|
|
||||||
|
const users = await User.find({
|
||||||
|
pinnedNoteId: { $exists: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
users.forEach(u => {
|
||||||
|
User.update({ _id: u._id }, {
|
||||||
|
$set: {
|
||||||
|
pinnedNoteIds: [(u as any).pinnedNoteId]
|
||||||
|
},
|
||||||
|
$unset: {
|
||||||
|
pinnedNoteId: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
type IUserBase = {
|
type IUserBase = {
|
||||||
_id: mongo.ObjectID;
|
_id: mongo.ObjectID;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
@@ -53,7 +75,7 @@ type IUserBase = {
|
|||||||
wallpaperUrl?: string;
|
wallpaperUrl?: string;
|
||||||
data: any;
|
data: any;
|
||||||
description: string;
|
description: string;
|
||||||
pinnedNoteId: mongo.ObjectID;
|
pinnedNoteIds: mongo.ObjectID[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 凍結されているか否か
|
* 凍結されているか否か
|
||||||
@@ -464,11 +486,11 @@ export const pack = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (opts.detail) {
|
if (opts.detail) {
|
||||||
if (_user.pinnedNoteId) {
|
if (_user.pinnedNoteIds) {
|
||||||
// Populate pinned note
|
// Populate pinned notes
|
||||||
_user.pinnedNote = packNote(_user.pinnedNoteId, meId, {
|
_user.pinnedNotes = Promise.all(_user.pinnedNoteIds.map((id: mongo.ObjectId) => packNote(id, meId, {
|
||||||
detail: true
|
detail: true
|
||||||
});
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meId && !meId.equals(_user.id)) {
|
if (meId && !meId.equals(_user.id)) {
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
9
src/remote/activitypub/renderer/add.ts
Normal file
9
src/remote/activitypub/renderer/add.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import config from '../../../config';
|
||||||
|
import { ILocalUser } from '../../../models/user';
|
||||||
|
|
||||||
|
export default (user: ILocalUser, target: any, object: any) => ({
|
||||||
|
type: 'Add',
|
||||||
|
actor: `${config.url}/users/${user._id}`,
|
||||||
|
target,
|
||||||
|
object
|
||||||
|
});
|
@@ -82,20 +82,38 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
|
|||||||
|
|
||||||
const files = await promisedFiles;
|
const files = await promisedFiles;
|
||||||
|
|
||||||
const text = note.text ? parseMfm(note.text).map(x => {
|
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) {
|
if (x.type == 'mention' && x.host == null) {
|
||||||
return `${x.content}@${config.host}`;
|
return `${x.content}@${config.host}`;
|
||||||
} else {
|
} else {
|
||||||
return x.content;
|
return x.content;
|
||||||
}
|
}
|
||||||
}).join('') : null;
|
}).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: text,
|
_misskey_content: text,
|
||||||
published: note.createdAt.toISOString(),
|
published: note.createdAt.toISOString(),
|
||||||
to,
|
to,
|
||||||
|
@@ -4,8 +4,9 @@
|
|||||||
* @param totalItems Total number of items
|
* @param totalItems Total number of items
|
||||||
* @param first URL of first page (optional)
|
* @param first URL of first page (optional)
|
||||||
* @param last URL of last page (optional)
|
* @param last URL of last page (optional)
|
||||||
|
* @param orderedItems attached objects (optional)
|
||||||
*/
|
*/
|
||||||
export default function(id: string, totalItems: any, first: string, last: string) {
|
export default function(id: string, totalItems: any, first?: string, last?: string, orderedItems?: object) {
|
||||||
const page: any = {
|
const page: any = {
|
||||||
id,
|
id,
|
||||||
type: 'OrderedCollection',
|
type: 'OrderedCollection',
|
||||||
@@ -14,6 +15,7 @@ export default function(id: string, totalItems: any, first: string, last: string
|
|||||||
|
|
||||||
if (first) page.first = first;
|
if (first) page.first = first;
|
||||||
if (last) page.last = last;
|
if (last) page.last = last;
|
||||||
|
if (orderedItems) page.orderedItems = orderedItems;
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@ export default async (user: ILocalUser) => {
|
|||||||
outbox: `${id}/outbox`,
|
outbox: `${id}/outbox`,
|
||||||
followers: `${id}/followers`,
|
followers: `${id}/followers`,
|
||||||
following: `${id}/following`,
|
following: `${id}/following`,
|
||||||
|
featured: `${id}/collections/featured`,
|
||||||
sharedInbox: `${config.url}/inbox`,
|
sharedInbox: `${config.url}/inbox`,
|
||||||
url: `${config.url}/@${user.username}`,
|
url: `${config.url}/@${user.username}`,
|
||||||
preferredUsername: user.username,
|
preferredUsername: user.username,
|
||||||
|
9
src/remote/activitypub/renderer/remove.ts
Normal file
9
src/remote/activitypub/renderer/remove.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import config from '../../../config';
|
||||||
|
import { ILocalUser } from '../../../models/user';
|
||||||
|
|
||||||
|
export default (user: ILocalUser, target: any, object: any) => ({
|
||||||
|
type: 'Remove',
|
||||||
|
actor: `${config.url}/users/${user._id}`,
|
||||||
|
target,
|
||||||
|
object
|
||||||
|
});
|
@@ -53,6 +53,7 @@ export interface IPerson extends IObject {
|
|||||||
publicKey: any;
|
publicKey: any;
|
||||||
followers: any;
|
followers: any;
|
||||||
following: any;
|
following: any;
|
||||||
|
featured?: any;
|
||||||
outbox: any;
|
outbox: any;
|
||||||
endpoints: string[];
|
endpoints: string[];
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ import renderPerson from '../remote/activitypub/renderer/person';
|
|||||||
import Outbox, { packActivity } from './activitypub/outbox';
|
import Outbox, { packActivity } from './activitypub/outbox';
|
||||||
import Followers from './activitypub/followers';
|
import Followers from './activitypub/followers';
|
||||||
import Following from './activitypub/following';
|
import Following from './activitypub/following';
|
||||||
|
import Featured from './activitypub/featured';
|
||||||
|
|
||||||
// Init router
|
// Init router
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
@@ -102,6 +103,9 @@ router.get('/users/:user/followers', Followers);
|
|||||||
// following
|
// following
|
||||||
router.get('/users/:user/following', Following);
|
router.get('/users/:user/following', Following);
|
||||||
|
|
||||||
|
// featured
|
||||||
|
router.get('/users/:user/collections/featured', Featured);
|
||||||
|
|
||||||
// publickey
|
// publickey
|
||||||
router.get('/users/:user/publickey', async ctx => {
|
router.get('/users/:user/publickey', async ctx => {
|
||||||
const userId = new mongo.ObjectID(ctx.params.user);
|
const userId = new mongo.ObjectID(ctx.params.user);
|
||||||
|
38
src/server/activitypub/featured.ts
Normal file
38
src/server/activitypub/featured.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import * as mongo from 'mongodb';
|
||||||
|
import * as Router from 'koa-router';
|
||||||
|
import config from '../../config';
|
||||||
|
import User from '../../models/user';
|
||||||
|
import pack from '../../remote/activitypub/renderer';
|
||||||
|
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
|
||||||
|
import { setResponseType } from '../activitypub';
|
||||||
|
import Note from '../../models/note';
|
||||||
|
import renderNote from '../../remote/activitypub/renderer/note';
|
||||||
|
|
||||||
|
export default async (ctx: Router.IRouterContext) => {
|
||||||
|
const userId = new mongo.ObjectID(ctx.params.user);
|
||||||
|
|
||||||
|
// Verify user
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: userId,
|
||||||
|
host: null
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
ctx.status = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinnedNoteIds = user.pinnedNoteIds || [];
|
||||||
|
|
||||||
|
const pinnedNotes = await Promise.all(pinnedNoteIds.map(id => Note.findOne({ _id: id })));
|
||||||
|
|
||||||
|
const renderedNotes = await Promise.all(pinnedNotes.map(note => renderNote(note)));
|
||||||
|
|
||||||
|
const rendered = renderOrderedCollection(
|
||||||
|
`${config.url}/users/${userId}/collections/featured`,
|
||||||
|
renderedNotes.length, null, null, renderedNotes
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.body = pack(rendered);
|
||||||
|
setResponseType(ctx);
|
||||||
|
};
|
@@ -1,7 +1,9 @@
|
|||||||
|
import * as mongo from 'mongodb';
|
||||||
import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
|
import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
|
||||||
import User, { ILocalUser } from '../../../../models/user';
|
import User, { ILocalUser } from '../../../../models/user';
|
||||||
import Note from '../../../../models/note';
|
import Note from '../../../../models/note';
|
||||||
import { pack } from '../../../../models/user';
|
import { pack } from '../../../../models/user';
|
||||||
|
import { deliverPinnedChange } from '../../../../services/i/pin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pin note
|
* Pin note
|
||||||
@@ -21,9 +23,25 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
|
|||||||
return rej('note not found');
|
return rej('note not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let addedId: mongo.ObjectID;
|
||||||
|
let removedId: mongo.ObjectID;
|
||||||
|
|
||||||
|
const pinnedNoteIds = user.pinnedNoteIds || [];
|
||||||
|
|
||||||
|
if (pinnedNoteIds.some(id => id.equals(note._id))) {
|
||||||
|
return rej('already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
pinnedNoteIds.unshift(note._id);
|
||||||
|
addedId = note._id;
|
||||||
|
|
||||||
|
if (pinnedNoteIds.length > 5) {
|
||||||
|
removedId = pinnedNoteIds.pop();
|
||||||
|
}
|
||||||
|
|
||||||
await User.update(user._id, {
|
await User.update(user._id, {
|
||||||
$set: {
|
$set: {
|
||||||
pinnedNoteId: note._id
|
pinnedNoteIds: pinnedNoteIds
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,6 +50,9 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
|
|||||||
detail: true
|
detail: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Send Add/Remove to followers
|
||||||
|
deliverPinnedChange(user._id, removedId, addedId);
|
||||||
|
|
||||||
// Send response
|
// Send response
|
||||||
res(iObj);
|
res(iObj);
|
||||||
});
|
});
|
||||||
|
@@ -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();
|
||||||
|
61
src/services/i/pin.ts
Normal file
61
src/services/i/pin.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import config from '../../config';
|
||||||
|
import * as mongo from 'mongodb';
|
||||||
|
import User, { isLocalUser, isRemoteUser, ILocalUser } from '../../models/user';
|
||||||
|
import Following from '../../models/following';
|
||||||
|
import renderAdd from '../../remote/activitypub/renderer/add';
|
||||||
|
import renderRemove from '../../remote/activitypub/renderer/remove';
|
||||||
|
import packAp from '../../remote/activitypub/renderer';
|
||||||
|
import { deliver } from '../../queue';
|
||||||
|
|
||||||
|
export async function deliverPinnedChange(userId: mongo.ObjectID, oldId?: mongo.ObjectID, newId?: mongo.ObjectID) {
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: userId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isLocalUser(user)) return;
|
||||||
|
|
||||||
|
const queue = await CreateRemoteInboxes(user);
|
||||||
|
|
||||||
|
if (queue.length < 1) return;
|
||||||
|
|
||||||
|
const target = `${config.url}/users/${user._id}/collections/featured`;
|
||||||
|
|
||||||
|
if (oldId) {
|
||||||
|
const oldItem = `${config.url}/notes/${oldId}`;
|
||||||
|
const content = packAp(renderRemove(user, target, oldItem));
|
||||||
|
queue.forEach(inbox => {
|
||||||
|
deliver(user, content, inbox);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newId) {
|
||||||
|
const newItem = `${config.url}/notes/${newId}`;
|
||||||
|
const content = packAp(renderAdd(user, target, newItem));
|
||||||
|
queue.forEach(inbox => {
|
||||||
|
deliver(user, content, inbox);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ローカルユーザーのリモートフォロワーのinboxリストを作成する
|
||||||
|
* @param user ローカルユーザー
|
||||||
|
*/
|
||||||
|
async function CreateRemoteInboxes(user: ILocalUser): Promise<string[]> {
|
||||||
|
const followers = await Following.find({
|
||||||
|
followeeId: user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
const queue: string[] = [];
|
||||||
|
|
||||||
|
followers.map(following => {
|
||||||
|
const follower = following._follower;
|
||||||
|
|
||||||
|
if (isRemoteUser(follower)) {
|
||||||
|
const inbox = follower.sharedInbox || follower.inbox;
|
||||||
|
if (!queue.includes(inbox)) queue.push(inbox);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return queue;
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
import es from '../../db/elasticsearch';
|
import 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