Compare commits
178 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
433dbe179d | |||
b21b33831a | |||
020cc471da | |||
43b47c4494 | |||
8751d91794 | |||
374b276f5c | |||
6138a74231 | |||
25438c4d64 | |||
ae6ce19886 | |||
e17a9bfd6f | |||
dc2055f5bc | |||
afeb8058b1 | |||
9299f99ac3 | |||
858fc7ebcc | |||
35089c65d3 | |||
643ca42829 | |||
935dc4fe33 | |||
3a9e74feb1 | |||
92e66fbf0c | |||
a50515f569 | |||
2f8f47acea | |||
dcb296db93 | |||
0bdae9ede7 | |||
11290c2a0f | |||
428b8f8669 | |||
7ced10f84e | |||
8ac54139c9 | |||
32afe77a26 | |||
6db8e33662 | |||
569561f247 | |||
d132d82acf | |||
9ba0db9372 | |||
5d468b542d | |||
32273165c7 | |||
46fdb75bf4 | |||
baf381814b | |||
e90387c14e | |||
876790d499 | |||
8b56edda4b | |||
33352256d6 | |||
e368ef11fa | |||
045f7c3185 | |||
bf40e5a5c5 | |||
cda3635d97 | |||
2eb561f132 | |||
b5f6465d61 | |||
9725076c46 | |||
f7228e79bb | |||
d3e250288a | |||
38f8043cb2 | |||
a61320ca8c | |||
4bc9bad34f | |||
4392e64672 | |||
74a0d60766 | |||
012a2b6b00 | |||
584bca7658 | |||
5dcd96d926 | |||
bd2be2815c | |||
2a5635492a | |||
eeea7527c1 | |||
d943a9a2f4 | |||
d4335f0e4d | |||
054f7cbdaa | |||
6ff95dab89 | |||
4461bde5da | |||
19152c28cb | |||
dda2967e2d | |||
a680bcda1f | |||
8d24fcba6a | |||
2a96429be8 | |||
5c6f376f4e | |||
1b24fad95f | |||
87743d9ef9 | |||
8ffd0abb1b | |||
2fed09ec18 | |||
6daabb35de | |||
59e98aa06c | |||
3601d95733 | |||
2c57dfd528 | |||
2348f2586c | |||
ed11f954aa | |||
5765a8e38e | |||
4a925fade1 | |||
fca86f43c4 | |||
12005de4c0 | |||
2e3d0d3435 | |||
7d76887517 | |||
bf39ecd1e5 | |||
7ebee09f74 | |||
952a49f749 | |||
8f8c67ad6d | |||
4257fed500 | |||
299f83684b | |||
ebeaef94e2 | |||
2399ba05cd | |||
8bcfa97349 | |||
dd3e3ddcdd | |||
2555e23b10 | |||
1595683904 | |||
d8dcc4da27 |
@ -78,7 +78,7 @@ gulp.task('build:copy', ['build:copy:views', 'build:copy:lang'], () =>
|
||||
]).pipe(gulp.dest('./built/'))
|
||||
);
|
||||
|
||||
gulp.task('test', ['lint', 'mocha']);
|
||||
gulp.task('test', ['mocha']);
|
||||
|
||||
gulp.task('lint', () =>
|
||||
gulp.src('./src/**/*.ts')
|
||||
|
@ -112,6 +112,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
reversi:
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "ホーム"
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "通知"
|
||||
list: "リスト"
|
||||
swap-left: "左に移動"
|
||||
@ -257,6 +261,9 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "正常に接続できるようです。ページを再度読み込みしてください。"
|
||||
flush: "キャッシュの削除"
|
||||
set-version: "バージョン指定"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
list: "リスト"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "おかえりなさい、"
|
||||
adjective: "さん"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||
mobile/views/pages/welcome.vue:
|
||||
|
@ -112,6 +112,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
reversi:
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "Startseite"
|
||||
local: "Lokal"
|
||||
hybrid: "ソーシャル"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "Global"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "Mitteilungen"
|
||||
list: "Listen"
|
||||
swap-left: "Nach links"
|
||||
@ -257,6 +261,9 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "Die Verbindung scheint zu funktionieren. Bitte lade die Seite neu."
|
||||
flush: "Cache leeren"
|
||||
set-version: "Version angeben"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "Lokal"
|
||||
hybrid: "ソーシャル"
|
||||
global: "Global"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
list: "Listen"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "おかえりなさい、"
|
||||
adjective: "さん"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||
mobile/views/pages/welcome.vue:
|
||||
|
@ -110,8 +110,9 @@ common:
|
||||
verified-user: "Verified account"
|
||||
disable-animated-mfm: "Disable animated texts in a post"
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
always-mark-nsfw: "Always post with a warning about media attachment"
|
||||
show-full-acct: "Do not omit the hostname from the username"
|
||||
reduce-motion: "Reduce motion in UI"
|
||||
this-setting-is-this-device-only: "Only for this device"
|
||||
do-not-use-in-production: 'As this is for development, do not use this in production.'
|
||||
reversi:
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "Home"
|
||||
local: "Local"
|
||||
hybrid: "Social"
|
||||
hashtag: "Hashtag"
|
||||
global: "Global"
|
||||
mentions: "Mentions"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "Notifications"
|
||||
list: "Lists"
|
||||
swap-left: "Move to the left"
|
||||
@ -257,6 +261,9 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "Looks like we have a connection. Please reload the page."
|
||||
flush: "Clean cache"
|
||||
set-version: "Specify version"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "NSFW"
|
||||
click-to-show: "Click to show"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "Hide"
|
||||
show: "See more"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "Local"
|
||||
hybrid: "Social"
|
||||
global: "Global"
|
||||
mentions: "Mentions"
|
||||
messages: "Messages"
|
||||
list: "Lists"
|
||||
hashtag: "Hashtag"
|
||||
add-tag-timeline: "Add hashtag tl"
|
||||
add-list: "Add list"
|
||||
list-name: "List name"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "Welcome back,"
|
||||
adjective: "-san"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "Local"
|
||||
hybrid: "Social"
|
||||
global: "Global"
|
||||
mentions: "Mentions"
|
||||
messages: "Messages"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "No posts \"{}\" found."
|
||||
mobile/views/pages/welcome.vue:
|
||||
|
@ -112,6 +112,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'Esto está en desarrollo, no usarlo para producción.'
|
||||
reversi:
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "Inicio"
|
||||
local: "Local"
|
||||
hybrid: "Social"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "Global"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "Notificaciones"
|
||||
list: "Listado"
|
||||
swap-left: "Desplazar a la izq."
|
||||
@ -257,6 +261,9 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "Parece que la conexión ha sido posible. Por favor refresca la página."
|
||||
flush: "Limpiar la memoria caché"
|
||||
set-version: "Escoge la versión"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
list: "リスト"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "Bienvenido/a de vuelta,"
|
||||
adjective: "-san"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||
mobile/views/pages/welcome.vue:
|
||||
|
@ -112,8 +112,9 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
reduce-motion: "Réduire les animations dans l’interface utilisateur"
|
||||
this-setting-is-this-device-only: "Uniquement sur cet appareil"
|
||||
do-not-use-in-production: 'Il s’agit d’une version de développement. Ne pas utiliser dans un environnement de production.'
|
||||
reversi:
|
||||
drawn: "Partie nulle"
|
||||
my-turn: "C’est votre tour"
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "Accueil"
|
||||
local: "Local"
|
||||
hybrid: "Social"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "Global"
|
||||
mentions: "Mentions"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "Notifications"
|
||||
list: "Liste"
|
||||
swap-left: "Déplacer à gauche"
|
||||
@ -257,9 +261,12 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "Succès de la connexion au serveur de Misskey. Veuillez recharger la page."
|
||||
flush: "Vider le cache"
|
||||
set-version: "Choisissez une version"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "Contenu sensible"
|
||||
click-to-show: "Cliquer pour afficher"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
hide: "Masquer"
|
||||
show: "Voir plus"
|
||||
common/views/components/messaging.vue:
|
||||
search-user: "Trouver un·e utilisateur·trice"
|
||||
you: "Vous"
|
||||
@ -465,7 +472,7 @@ desktop/views/components/charts.vue:
|
||||
notes: "Publications"
|
||||
users: "Utilisateurs"
|
||||
drive: "Drive"
|
||||
network: "ネットワーク"
|
||||
network: "Réseau"
|
||||
charts:
|
||||
notes: "投稿の増減 (統合)"
|
||||
local-notes: "投稿の増減 (ローカル)"
|
||||
@ -477,9 +484,9 @@ desktop/views/components/charts.vue:
|
||||
drive-total: "ドライブ使用量の累計"
|
||||
drive-files: "ドライブのファイル数の増減"
|
||||
drive-files-total: "ドライブのファイル数の累計"
|
||||
network-requests: "リクエスト"
|
||||
network-time: "応答時間"
|
||||
network-usage: "通信量"
|
||||
network-requests: "Requêtes"
|
||||
network-time: "Temps de réponse"
|
||||
network-usage: "Traffic"
|
||||
desktop/views/components/choose-file-from-drive-window.vue:
|
||||
choose-file: "Sélection de fichiers"
|
||||
upload: "Téléverser des fichiers à partir de votre ordinateur"
|
||||
@ -673,7 +680,7 @@ desktop/views/components/settings.vue:
|
||||
fetch-on-scroll-desc: "Chargement automatique du contenu lors du défilement de la page."
|
||||
note-visibility: "Visibilité de la publication"
|
||||
default-note-visibility: "Visibilité par défaut"
|
||||
remember-note-visibility: "投稿の公開範囲を記憶する"
|
||||
remember-note-visibility: "Se souvenir du mode de visibilité de la publication"
|
||||
auto-popout: "Fenêtre contextuelle automatique"
|
||||
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
|
||||
advanced: "Paramètres avancés"
|
||||
@ -786,7 +793,7 @@ desktop/views/components/settings.profile.vue:
|
||||
birthday: "Date de naissance"
|
||||
save: "Mettre à jour le profil"
|
||||
locked-account: "Protéger votre compte"
|
||||
is-locked: "フォローを承認制にする"
|
||||
is-locked: "Demande d’abonnement en attente d’approbation"
|
||||
other: "Autre"
|
||||
is-bot: "Ce compte est un Bot"
|
||||
is-cat: "Ce compte est un Chat"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "Local"
|
||||
hybrid: "Social"
|
||||
global: "Global"
|
||||
mentions: "Mentions"
|
||||
messages: "メッセージ"
|
||||
list: "Listes"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "Content de vous revoir !"
|
||||
adjective: "さん"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "Local"
|
||||
hybrid: "Social"
|
||||
global: "Global"
|
||||
mentions: "Mentions"
|
||||
messages: "メッセージ"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "Pas de message avec un hashtag {} trouvé."
|
||||
mobile/views/pages/welcome.vue:
|
||||
@ -1168,9 +1183,9 @@ mobile/views/pages/settings/settings.profile.vue:
|
||||
avatar: "Avatar"
|
||||
banner: "Bannière"
|
||||
is-cat: "Ce compte est un Bot"
|
||||
is-locked: "フォローを承認制にする"
|
||||
advanced: "その他"
|
||||
privacy: "プライバシー"
|
||||
is-locked: "Demande d’abonnement en attente d’approbation"
|
||||
advanced: "Avancé"
|
||||
privacy: "Vie privée"
|
||||
save: "Mettre à jour le profil"
|
||||
saved: "Profil mis à jour avec succès"
|
||||
uploading: "En cours d'envoi"
|
||||
@ -1192,7 +1207,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "Mode nuit"
|
||||
i-am-under-limited-internet: "J'ai un accès Internet limité"
|
||||
circle-icons: "Utiliser des icônes circulaires"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
contrasted-acct: "Nom d’utilisateur contrasté"
|
||||
timeline: "Fil d'actualité"
|
||||
show-reply-target: "Afficher les réponses"
|
||||
show-my-renotes: "Afficher mes republications"
|
||||
|
@ -112,6 +112,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
reversi:
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "ホーム"
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "通知"
|
||||
list: "リスト"
|
||||
swap-left: "左に移動"
|
||||
@ -257,6 +261,9 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "正常に接続できるようです。ページを再度読み込みしてください。"
|
||||
flush: "キャッシュの削除"
|
||||
set-version: "バージョン指定"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
list: "リスト"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "おかえりなさい、"
|
||||
adjective: "さん"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||
mobile/views/pages/welcome.vue:
|
||||
|
@ -119,6 +119,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
@ -165,7 +166,10 @@ common:
|
||||
home: "ホーム"
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "通知"
|
||||
list: "リスト"
|
||||
swap-left: "左に移動"
|
||||
@ -277,6 +281,10 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
flush: "キャッシュの削除"
|
||||
set-version: "バージョン指定"
|
||||
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
@ -908,7 +916,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
list: "リスト"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "おかえりなさい、"
|
||||
@ -1309,6 +1323,8 @@ mobile/views/pages/home.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||
|
@ -112,6 +112,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
reversi:
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "うち"
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "通知"
|
||||
list: "リスト"
|
||||
swap-left: "左に移動や!"
|
||||
@ -257,6 +261,9 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "正常に接続できるようやわ。ページを再度読み込みしてな。"
|
||||
flush: "キャッシュの削除"
|
||||
set-version: "バージョン指定"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
list: "リスト"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "おかえり、"
|
||||
adjective: "さん"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿はあらへんで。"
|
||||
mobile/views/pages/welcome.vue:
|
||||
|
@ -112,6 +112,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
reversi:
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "홈"
|
||||
local: "로컬"
|
||||
hybrid: "소셜"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "글로벌"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "통지"
|
||||
list: "목록"
|
||||
swap-left: "左に移動"
|
||||
@ -257,6 +261,9 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "正常に接続できるようです。ページを再度読み込みしてください。"
|
||||
flush: "キャッシュの削除"
|
||||
set-version: "バージョン指定"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
list: "リスト"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "おかえりなさい、"
|
||||
adjective: "さん"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||
mobile/views/pages/welcome.vue:
|
||||
|
@ -112,6 +112,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
reversi:
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "ホーム"
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "通知"
|
||||
list: "リスト"
|
||||
swap-left: "左に移動"
|
||||
@ -257,6 +261,9 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "Het verbinden lijkt te lukken. Herlaad de pagina."
|
||||
flush: "Cache leegmaken"
|
||||
set-version: "Versie opgeven"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "Lokaal"
|
||||
hybrid: "ソーシャル"
|
||||
global: "Algemeen"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
list: "Lijsten"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "おかえりなさい、"
|
||||
adjective: "さん"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||
mobile/views/pages/welcome.vue:
|
||||
|
@ -112,6 +112,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
reversi:
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "ホーム"
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "通知"
|
||||
list: "リスト"
|
||||
swap-left: "左に移動"
|
||||
@ -257,6 +261,9 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "正常に接続できるようです。ページを再度読み込みしてください。"
|
||||
flush: "キャッシュの削除"
|
||||
set-version: "バージョン指定"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
list: "リスト"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "おかえりなさい、"
|
||||
adjective: "さん"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||
mobile/views/pages/welcome.vue:
|
||||
|
@ -112,6 +112,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
reversi:
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "Strona główna"
|
||||
local: "Lokalne"
|
||||
hybrid: "Społeczność"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "Globalne"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "Powiadomienia"
|
||||
list: "Listy"
|
||||
swap-left: "Przesuń w lewo"
|
||||
@ -257,6 +261,9 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "Wygląda na to, że udało się połączyć. Odśwież stronę."
|
||||
flush: "Wyczyść pamięć podręczną"
|
||||
set-version: "Określ wersję"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "Lokalne"
|
||||
hybrid: "Społeczność"
|
||||
global: "Globalne"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
list: "Listy"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "Witaj ponownie,"
|
||||
adjective: "さん"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "Lokalne"
|
||||
hybrid: "Społeczność"
|
||||
global: "Globalne"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "Nie znaleziono wpisów zawierających „{}”."
|
||||
mobile/views/pages/welcome.vue:
|
||||
|
@ -7,12 +7,12 @@ common:
|
||||
about-title: "Uma ⭐ do fediverso."
|
||||
about: "Obrigado por encontrar Misskey. Uma <b>plataforma descentralizada de microblog</b> nascida na Terra. Já que ela existe no Fediverso (um universo onde várias plataformas de mídia social são organizadas), ela é ligada com outras plataformas.Por que você não tira uma folga do agito e confusão da cidade, e mergulha em uma nova internet?"
|
||||
intro:
|
||||
title: "Misskeyって?"
|
||||
about: "Misskeyはオープンソースの<b>分散型マイクロブログSNS</b>です。リッチで高度にカスタマイズできるUI、投稿へのリアクション、ファイルを一元管理できるドライブなど、先進的な機能を揃えています。また、Fediverseと呼ばれるネットワークに接続できるため、他のSNSともやり取りできます。例えば、あなたが何か投稿すると、その投稿はMisskeyだけでなく他のSNSにも伝わります。ちょうどある惑星から他の惑星に電波を発信している様子をイメージしてください。"
|
||||
features: "特徴"
|
||||
rich-contents: "投稿"
|
||||
rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。"
|
||||
reaction: "リアクション"
|
||||
title: "O que é Misskey?"
|
||||
about: "Misskey é um <b>serviço de microblog descentralizado</b>. Personalização sofisticada da interface, variedade de reações a posts, armazenamento de arquivos grátis com gerenciamento integrado e outras funções avançadas estão disponíveis. Um sistema em rede chamado \"Fediverso\" permite que nos comuniquemos com usuários em outras redes sociais. Se você postar algo, por exemplo, seu post não será mandado apenas para o Misskey, mas também para o Mastodon. Apenas imagine que o planeta está enviando ondas de rádio para outros planetas para se comunicar."
|
||||
features: "Recursos"
|
||||
rich-contents: "Post"
|
||||
rich-contents-desc: "Apenas poste suas ideias, temas do momento e qualquer coisa que você queira compartilhar. Você pode querer decorar suas palavras, anexar suas imagens favoritas, enviar arquivos, inclusive vídeos ou criar uma enquete. Essas são as coisas que você pode fazer em Misskey."
|
||||
reaction: "Reações"
|
||||
reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
|
||||
ui: "インターフェース"
|
||||
ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。"
|
||||
@ -112,6 +112,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
reversi:
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "Início"
|
||||
local: "Local"
|
||||
hybrid: "Social"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "Global"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "Notificações"
|
||||
list: "Listas"
|
||||
swap-left: "Mover para a esquerda"
|
||||
@ -257,6 +261,9 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "正常に接続できるようです。ページを再度読み込みしてください。"
|
||||
flush: "Limpar o cache"
|
||||
set-version: "バージョン指定"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
list: "リスト"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "おかえりなさい、"
|
||||
adjective: "さん"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||
mobile/views/pages/welcome.vue:
|
||||
@ -1160,23 +1175,23 @@ mobile/views/pages/games/reversi.vue:
|
||||
reversi: "リバーシ"
|
||||
mobile/views/pages/settings/settings.profile.vue:
|
||||
title: "プロフィール"
|
||||
name: "名前"
|
||||
account: "アカウント"
|
||||
location: "場所"
|
||||
description: "自己紹介"
|
||||
birthday: "誕生日"
|
||||
avatar: "アイコン"
|
||||
banner: "バナー"
|
||||
is-cat: "このアカウントはCatです"
|
||||
is-locked: "フォローを承認制にする"
|
||||
advanced: "その他"
|
||||
privacy: "プライバシー"
|
||||
save: "保存"
|
||||
saved: "プロフィールを保存しました"
|
||||
uploading: "アップロード中"
|
||||
upload-failed: "アップロードに失敗しました"
|
||||
name: "Nome"
|
||||
account: "Conta"
|
||||
location: "Lugar"
|
||||
description: "Biografia"
|
||||
birthday: "Data de nascimento"
|
||||
avatar: "Avatar"
|
||||
banner: "Capa"
|
||||
is-cat: "Esta conta é gato"
|
||||
is-locked: "Pedido para seguir precisa ser aprovado"
|
||||
advanced: "Avançado"
|
||||
privacy: "Provacidade"
|
||||
save: "Atualizar perfil"
|
||||
saved: "Perfil atualizado"
|
||||
uploading: "Enviando"
|
||||
upload-failed: "Falha ao enviar"
|
||||
mobile/views/pages/search.vue:
|
||||
search: "検索"
|
||||
search: "Pesquisar"
|
||||
empty: "「{}」に関する投稿は見つかりませんでした。"
|
||||
not-found: "「{}」に関する投稿は見つかりませんでした。"
|
||||
mobile/views/pages/selectdrive.vue:
|
||||
@ -1213,47 +1228,47 @@ mobile/views/pages/settings.vue:
|
||||
load-raw-images: "添付された画像を高画質で表示する"
|
||||
load-remote-media: "リモートサーバーのメディアを表示する"
|
||||
twitter: "Twitter連携"
|
||||
twitter-connect: "Twitterアカウントに接続する"
|
||||
twitter-reconnect: "再接続する"
|
||||
twitter-disconnect: "切断する"
|
||||
update: "Misskey Update"
|
||||
version: "バージョン:"
|
||||
latest-version: "最新のバージョン:"
|
||||
update-checking: "アップデートを確認中"
|
||||
check-for-updates: "アップデートを確認"
|
||||
no-updates: "利用可能な更新はありません"
|
||||
no-updates-desc: "お使いのMisskeyは最新です。"
|
||||
update-available: "新しいバージョンが利用可能です"
|
||||
update-available-desc: "ページを再度読み込みすると更新が適用されます。"
|
||||
settings: "設定"
|
||||
signout: "サインアウト"
|
||||
sound: "サウンド"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
twitter-connect: "Conectar à sua conta no Twitter"
|
||||
twitter-reconnect: "Reconectar"
|
||||
twitter-disconnect: "Desconectar"
|
||||
update: "Atualizar Misskey"
|
||||
version: "Versão atual;"
|
||||
latest-version: "Última versão:"
|
||||
update-checking: "Verificando atualizações"
|
||||
check-for-updates: "Verificar atualizações"
|
||||
no-updates: "Sem atualizações"
|
||||
no-updates-desc: "Seu Misskey está atualizado"
|
||||
update-available: "Uma nova versão está disponível"
|
||||
update-available-desc: "Atualizações vão ser aplicadas depois de recarregar a página"
|
||||
settings: "Configurações"
|
||||
signout: "Sair"
|
||||
sound: "Sons"
|
||||
enable-sounds: "Ativar sons"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "フォローされています"
|
||||
following: "フォロー"
|
||||
followers: "フォロワー"
|
||||
notes: "投稿"
|
||||
follows-you: "Te segue"
|
||||
following: "Seguindo"
|
||||
followers: "Seguidores"
|
||||
notes: "Posts"
|
||||
overview: "概要"
|
||||
timeline: "タイムライン"
|
||||
media: "メディア"
|
||||
is-suspended: "このユーザーは凍結されています。"
|
||||
timeline: "Linha do tempo"
|
||||
media: "Mídia"
|
||||
is-suspended: "Esta conta foi suspensa"
|
||||
is-remote: "Este é uma usuário remoto. O perfil que vê aqui pode não estar completo."
|
||||
view-remote: "Ver o perfil completo."
|
||||
mobile/views/pages/user/home.vue:
|
||||
recent-notes: "Notas recentes"
|
||||
images: "Imagens"
|
||||
activity: "Atividade"
|
||||
keywords: "キーワード"
|
||||
domains: "頻出ドメイン"
|
||||
frequently-replied-users: "よく会話するユーザー"
|
||||
keywords: "Palavras chave"
|
||||
domains: "Domínios"
|
||||
frequently-replied-users: "Perguntas frequentes"
|
||||
followers-you-know: "Seguidores que você conhece"
|
||||
last-used-at: "Ativo pela última vez:"
|
||||
mobile/views/pages/user/home.followers-you-know.vue:
|
||||
loading: "Carregando"
|
||||
no-users: "知り合いのユーザーはいません"
|
||||
mobile/views/pages/user/home.friends.vue:
|
||||
loading: "読み込み中"
|
||||
loading: "Carregando"
|
||||
no-users: "よく会話するユーザーはいません"
|
||||
mobile/views/pages/user/home.notes.vue:
|
||||
loading: "Carregando"
|
||||
@ -1263,14 +1278,14 @@ mobile/views/pages/user/home.photos.vue:
|
||||
no-photos: "Sem fotos"
|
||||
docs:
|
||||
edit-this-page-on-github: "間違いや改善点を見つけましたか?"
|
||||
edit-this-page-on-github-link: "このページをGitHubで編集"
|
||||
edit-this-page-on-github-link: "Edite esta página no GitHub!"
|
||||
api:
|
||||
entities:
|
||||
properties: "プロパティ"
|
||||
properties: "Propriedades"
|
||||
endpoints:
|
||||
params: "パラメータ"
|
||||
no-params: "パラメータはありません"
|
||||
res: "レスポンス"
|
||||
params: "Parâmetros"
|
||||
no-params: "Sem parâmetros"
|
||||
res: "Resposta"
|
||||
require-credential: "このエンドポイントは認証情報が必須です。"
|
||||
require-permission: "このエンドポイントは{permission}の権限を必要とします。"
|
||||
has-limit: "レートリミットがあります。"
|
||||
|
@ -112,6 +112,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
reversi:
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "ホーム"
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "通知"
|
||||
list: "リスト"
|
||||
swap-left: "左に移動"
|
||||
@ -257,6 +261,9 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "正常に接続できるようです。ページを再度読み込みしてください。"
|
||||
flush: "キャッシュの削除"
|
||||
set-version: "バージョン指定"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
list: "リスト"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "おかえりなさい、"
|
||||
adjective: "さん"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||
mobile/views/pages/welcome.vue:
|
||||
|
@ -112,6 +112,7 @@ common:
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
reversi:
|
||||
@ -154,7 +155,10 @@ common:
|
||||
home: "ホーム"
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
hashtag: "ハッシュタグ"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "通知"
|
||||
list: "リスト"
|
||||
swap-left: "左に移動"
|
||||
@ -257,6 +261,9 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
success-desc: "正常に接続できるようです。ページを再度読み込みしてください。"
|
||||
flush: "キャッシュの削除"
|
||||
set-version: "バージョン指定"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "隠す"
|
||||
show: "もっと見る"
|
||||
@ -803,7 +810,13 @@ desktop/views/components/timeline.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
list: "リスト"
|
||||
hashtag: "ハッシュタグ"
|
||||
add-tag-timeline: "ハッシュタグを追加"
|
||||
add-list: "リストを追加"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "おかえりなさい、"
|
||||
adjective: "さん"
|
||||
@ -1128,6 +1141,8 @@ mobile/views/pages/home.vue:
|
||||
local: "ローカル"
|
||||
hybrid: "ソーシャル"
|
||||
global: "グローバル"
|
||||
mentions: "あなた宛て"
|
||||
messages: "メッセージ"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
|
||||
mobile/views/pages/welcome.vue:
|
||||
|
16
package.json
16
package.json
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "8.41.0",
|
||||
"clientVersion": "1.0.9716",
|
||||
"version": "8.52.0",
|
||||
"clientVersion": "1.0.9894",
|
||||
"codename": "nighthike",
|
||||
"main": "./built/index.js",
|
||||
"private": true,
|
||||
@ -20,10 +20,10 @@
|
||||
"format": "gulp format"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": "1.1.8",
|
||||
"@fortawesome/fontawesome-free-brands": "5.0.13",
|
||||
"@fortawesome/fontawesome-free-regular": "5.0.13",
|
||||
"@fortawesome/fontawesome-free-solid": "5.0.13",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.4",
|
||||
"@fortawesome/free-brands-svg-icons": "5.3.1",
|
||||
"@fortawesome/free-regular-svg-icons": "5.3.1",
|
||||
"@fortawesome/free-solid-svg-icons": "5.3.1",
|
||||
"@koa/cors": "2.2.2",
|
||||
"@prezzemolo/rap": "0.1.2",
|
||||
"@prezzemolo/zip": "0.0.3",
|
||||
@ -60,7 +60,7 @@
|
||||
"@types/mocha": "5.2.3",
|
||||
"@types/mongodb": "3.1.7",
|
||||
"@types/ms": "0.7.30",
|
||||
"@types/node": "10.9.4",
|
||||
"@types/node": "10.10.1",
|
||||
"@types/portscanner": "2.1.0",
|
||||
"@types/pug": "2.0.4",
|
||||
"@types/qrcode": "1.2.0",
|
||||
@ -206,7 +206,7 @@
|
||||
"v-animate-css": "0.0.2",
|
||||
"vue": "2.5.17",
|
||||
"vue-chartjs": "3.4.0",
|
||||
"vue-cropperjs": "2.2.1",
|
||||
"vue-cropperjs": "2.2.2",
|
||||
"vue-js-modal": "1.3.26",
|
||||
"vue-json-tree-view": "2.1.4",
|
||||
"vue-loader": "15.4.2",
|
||||
|
106
src/client/app/common/hotkey.ts
Normal file
106
src/client/app/common/hotkey.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import keyCode from './keycode';
|
||||
import { concat } from '../../../prelude/array';
|
||||
|
||||
type pattern = {
|
||||
which: string[];
|
||||
ctrl?: boolean;
|
||||
shift?: boolean;
|
||||
alt?: boolean;
|
||||
};
|
||||
|
||||
type action = {
|
||||
patterns: pattern[];
|
||||
|
||||
callback: Function;
|
||||
};
|
||||
|
||||
const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => {
|
||||
const result = {
|
||||
patterns: [],
|
||||
callback: callback
|
||||
} as action;
|
||||
|
||||
result.patterns = patterns.split('|').map(part => {
|
||||
const pattern = {
|
||||
which: []
|
||||
} as pattern;
|
||||
|
||||
part.trim().split('+').forEach(key => {
|
||||
key = key.trim().toLowerCase();
|
||||
switch (key) {
|
||||
case 'ctrl': pattern.ctrl = true; break;
|
||||
case 'alt': pattern.alt = true; break;
|
||||
case 'shift': pattern.shift = true; break;
|
||||
default: pattern.which = keyCode(key).map(k => k.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
return pattern;
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const ignoreElemens = ['input', 'textarea'];
|
||||
|
||||
export default {
|
||||
install(Vue) {
|
||||
Vue.directive('hotkey', {
|
||||
bind(el, binding) {
|
||||
el._hotkey_global = binding.modifiers.global === true;
|
||||
|
||||
const actions = getKeyMap(binding.value);
|
||||
|
||||
// flatten
|
||||
const reservedKeys = concat(concat(actions.map(a => a.patterns.map(p => p.which))));
|
||||
|
||||
el.dataset.reservedKeys = reservedKeys.map(key => `'${key}'`).join(' ');
|
||||
|
||||
el._keyHandler = e => {
|
||||
const key = e.code.toLowerCase();
|
||||
|
||||
const targetReservedKeys = document.activeElement ? ((document.activeElement as any).dataset || {}).reservedKeys || '' : '';
|
||||
if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
|
||||
|
||||
for (const action of actions) {
|
||||
if (el._hotkey_global && targetReservedKeys.includes(`'${key}'`)) break;
|
||||
|
||||
const matched = action.patterns.some(pattern => {
|
||||
let matched = pattern.which.includes(key);
|
||||
if (pattern.ctrl && !e.ctrlKey) matched = false;
|
||||
if (pattern.shift && !e.shiftKey) matched = false;
|
||||
if (pattern.alt && !e.altKey) matched = false;
|
||||
|
||||
if (matched) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
action.callback(e);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (matched) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (el._hotkey_global) {
|
||||
document.addEventListener('keydown', el._keyHandler);
|
||||
} else {
|
||||
el.addEventListener('keydown', el._keyHandler);
|
||||
}
|
||||
},
|
||||
|
||||
unbind(el) {
|
||||
if (el._hotkey_global) {
|
||||
document.removeEventListener('keydown', el._keyHandler);
|
||||
} else {
|
||||
el.removeEventListener('keydown', el._keyHandler);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
33
src/client/app/common/keycode.ts
Normal file
33
src/client/app/common/keycode.ts
Normal file
@ -0,0 +1,33 @@
|
||||
export default (input: string): string[] => {
|
||||
if (Object.keys(aliases).some(a => a.toLowerCase() == input.toLowerCase())) {
|
||||
const codes = aliases[input];
|
||||
return Array.isArray(codes) ? codes : [codes];
|
||||
} else {
|
||||
return [input];
|
||||
}
|
||||
};
|
||||
|
||||
export const aliases = {
|
||||
'esc': 'Escape',
|
||||
'enter': ['Enter', 'NumpadEnter'],
|
||||
'up': 'ArrowUp',
|
||||
'down': 'ArrowDown',
|
||||
'left': 'ArrowLeft',
|
||||
'right': 'ArrowRight',
|
||||
'plus': ['NumpadAdd', 'Semicolon'],
|
||||
};
|
||||
|
||||
/*!
|
||||
* Programatically add the following
|
||||
*/
|
||||
|
||||
// lower case chars
|
||||
for (let i = 97; i < 123; i++) {
|
||||
const char = String.fromCharCode(i);
|
||||
aliases[char] = `Key${char.toUpperCase()}`;
|
||||
}
|
||||
|
||||
// numbers
|
||||
for (let i = 0; i < 10; i++) {
|
||||
aliases[i] = [`Numpad${i}`, `Digit${i}`];
|
||||
}
|
13
src/client/app/common/scripts/streaming/hashtag.ts
Normal file
13
src/client/app/common/scripts/streaming/hashtag.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import Stream from './stream';
|
||||
import MiOS from '../../../mios';
|
||||
|
||||
export class HashtagStream extends Stream {
|
||||
constructor(os: MiOS, me, q) {
|
||||
super(os, 'hashtag', me ? {
|
||||
i: me.token,
|
||||
q: JSON.stringify(q)
|
||||
} : {
|
||||
q: JSON.stringify(q)
|
||||
});
|
||||
}
|
||||
}
|
90
src/client/app/common/views/components/media-banner.vue
Normal file
90
src/client/app/common/views/components/media-banner.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="mk-media-banner">
|
||||
<div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false">
|
||||
<span class="icon">%fa:exclamation-triangle%</span>
|
||||
<b>%i18n:@sensitive%</b>
|
||||
<span>%i18n:@click-to-show%</span>
|
||||
</div>
|
||||
<div class="audio" v-else-if="media.type.startsWith('audio')">
|
||||
<audio class="audio"
|
||||
:src="media.url"
|
||||
:title="media.name"
|
||||
controls
|
||||
ref="audio"
|
||||
preload="metadata" />
|
||||
</div>
|
||||
<a class="download" v-else
|
||||
:href="media.url"
|
||||
:title="media.name"
|
||||
:download="media.name"
|
||||
>
|
||||
<span class="icon">%fa:download%</span>
|
||||
<b>{{ media.name }}</b>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
media: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hide: true
|
||||
};
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
root(isDark)
|
||||
width 100%
|
||||
border-radius 4px
|
||||
margin-top 4px
|
||||
overflow hidden
|
||||
|
||||
> .download,
|
||||
> .sensitive
|
||||
display flex
|
||||
align-items center
|
||||
font-size 12px
|
||||
padding 8px 12px
|
||||
white-space nowrap
|
||||
|
||||
> *
|
||||
display block
|
||||
|
||||
> b
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
|
||||
> *:not(:last-child)
|
||||
margin-right .2em
|
||||
|
||||
> .icon
|
||||
font-size 1.6em
|
||||
|
||||
> .download
|
||||
background isDark ? #21242d : #f7f7f7
|
||||
|
||||
> .sensitive
|
||||
background #111
|
||||
color #fff
|
||||
|
||||
> .audio
|
||||
.audio
|
||||
display block
|
||||
width 100%
|
||||
|
||||
.mk-media-banner[data-darkmode]
|
||||
root(true)
|
||||
|
||||
.mk-media-banner:not([data-darkmode])
|
||||
root(false)
|
||||
</style>
|
@ -1,18 +1,27 @@
|
||||
<template>
|
||||
<div class="mk-media-list">
|
||||
<div :data-count="mediaList.length" ref="grid">
|
||||
<template v-for="media in mediaList">
|
||||
<mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/>
|
||||
<mk-media-image :image="media" :key="media.id" v-else :raw="raw"/>
|
||||
</template>
|
||||
<template v-for="media in mediaList.filter(media => !previewable(media))">
|
||||
<x-banner :media="media" :key="media.id"/>
|
||||
</template>
|
||||
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
|
||||
<div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid">
|
||||
<template v-for="media in mediaList">
|
||||
<mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
|
||||
<mk-media-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XBanner from './media-banner.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XBanner
|
||||
},
|
||||
props: {
|
||||
mediaList: {
|
||||
required: true
|
||||
@ -22,70 +31,80 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// for Safari bug
|
||||
this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px';
|
||||
//#region for Safari bug
|
||||
if (this.$refs.grid) {
|
||||
this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px';
|
||||
}
|
||||
//#endregion
|
||||
},
|
||||
methods: {
|
||||
previewable(file) {
|
||||
return file.type.startsWith('video') || file.type.startsWith('image');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-media-list
|
||||
width 100%
|
||||
> .gird-container
|
||||
width 100%
|
||||
margin-top 4px
|
||||
|
||||
&:before
|
||||
content ''
|
||||
display block
|
||||
padding-top 56.25% // 16:9
|
||||
&:before
|
||||
content ''
|
||||
display block
|
||||
padding-top 56.25% // 16:9
|
||||
|
||||
> div
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
bottom 0
|
||||
left 0
|
||||
display grid
|
||||
grid-gap 4px
|
||||
> div
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
bottom 0
|
||||
left 0
|
||||
display grid
|
||||
grid-gap 4px
|
||||
|
||||
> *
|
||||
overflow hidden
|
||||
border-radius 4px
|
||||
> *
|
||||
overflow hidden
|
||||
border-radius 4px
|
||||
|
||||
&[data-count="1"]
|
||||
grid-template-rows 1fr
|
||||
&[data-count="1"]
|
||||
grid-template-rows 1fr
|
||||
|
||||
&[data-count="2"]
|
||||
grid-template-columns 1fr 1fr
|
||||
grid-template-rows 1fr
|
||||
&[data-count="2"]
|
||||
grid-template-columns 1fr 1fr
|
||||
grid-template-rows 1fr
|
||||
|
||||
&[data-count="3"]
|
||||
grid-template-columns 1fr 0.5fr
|
||||
grid-template-rows 1fr 1fr
|
||||
&[data-count="3"]
|
||||
grid-template-columns 1fr 0.5fr
|
||||
grid-template-rows 1fr 1fr
|
||||
|
||||
> *:nth-child(1)
|
||||
grid-row 1 / 3
|
||||
|
||||
> *:nth-child(3)
|
||||
grid-column 2 / 3
|
||||
grid-row 2 / 3
|
||||
|
||||
&[data-count="4"]
|
||||
grid-template-columns 1fr 1fr
|
||||
grid-template-rows 1fr 1fr
|
||||
|
||||
> *:nth-child(1)
|
||||
grid-row 1 / 3
|
||||
grid-column 1 / 2
|
||||
grid-row 1 / 2
|
||||
|
||||
> *:nth-child(2)
|
||||
grid-column 2 / 3
|
||||
grid-row 1 / 2
|
||||
|
||||
> *:nth-child(3)
|
||||
grid-column 1 / 2
|
||||
grid-row 2 / 3
|
||||
|
||||
> *:nth-child(4)
|
||||
grid-column 2 / 3
|
||||
grid-row 2 / 3
|
||||
|
||||
&[data-count="4"]
|
||||
grid-template-columns 1fr 1fr
|
||||
grid-template-rows 1fr 1fr
|
||||
|
||||
> *:nth-child(1)
|
||||
grid-column 1 / 2
|
||||
grid-row 1 / 2
|
||||
|
||||
> *:nth-child(2)
|
||||
grid-column 2 / 3
|
||||
grid-row 1 / 2
|
||||
|
||||
> *:nth-child(3)
|
||||
grid-column 1 / 2
|
||||
grid-row 2 / 3
|
||||
|
||||
> *:nth-child(4)
|
||||
grid-column 2 / 3
|
||||
grid-row 2 / 3
|
||||
|
||||
</style>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="mk-reaction-picker">
|
||||
<div class="mk-reaction-picker" v-hotkey.global="keymap">
|
||||
<div class="backdrop" ref="backdrop" @click="close"></div>
|
||||
<div class="popover" :class="{ compact, big }" ref="popover">
|
||||
<p v-if="!compact">{{ title }}</p>
|
||||
<div>
|
||||
<div ref="buttons" :class="{ showFocus }">
|
||||
<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
|
||||
<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button>
|
||||
<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button>
|
||||
@ -31,30 +31,84 @@ export default Vue.extend({
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
|
||||
source: {
|
||||
required: true
|
||||
},
|
||||
|
||||
compact: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
|
||||
cb: {
|
||||
required: false
|
||||
},
|
||||
|
||||
big: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
|
||||
showFocus: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
|
||||
animation: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
title: placeholder
|
||||
title: placeholder,
|
||||
focus: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'esc': this.close,
|
||||
'enter|space|plus': this.choose,
|
||||
'up|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.childNodes[i].focus();
|
||||
|
||||
if (this.showFocus) {
|
||||
this.title = this.$refs.buttons.childNodes[i].title;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.focus = 0;
|
||||
|
||||
const popover = this.$refs.popover as any;
|
||||
|
||||
const rect = this.source.getBoundingClientRect();
|
||||
@ -76,7 +130,7 @@ export default Vue.extend({
|
||||
anime({
|
||||
targets: this.$refs.backdrop,
|
||||
opacity: 1,
|
||||
duration: 100,
|
||||
duration: this.animation ? 100 : 0,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
||||
@ -84,10 +138,11 @@ export default Vue.extend({
|
||||
targets: this.$refs.popover,
|
||||
opacity: 1,
|
||||
scale: [0.5, 1],
|
||||
duration: 500
|
||||
duration: this.animation ? 500 : 0
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
react(reaction) {
|
||||
(this as any).api('notes/reactions/create', {
|
||||
@ -95,21 +150,25 @@ export default Vue.extend({
|
||||
reaction: reaction
|
||||
}).then(() => {
|
||||
if (this.cb) this.cb();
|
||||
this.$emit('closed');
|
||||
this.destroyDom();
|
||||
});
|
||||
},
|
||||
|
||||
onMouseover(e) {
|
||||
this.title = e.target.title;
|
||||
},
|
||||
|
||||
onMouseout(e) {
|
||||
this.title = placeholder;
|
||||
},
|
||||
|
||||
close() {
|
||||
(this.$refs.backdrop as any).style.pointerEvents = 'none';
|
||||
anime({
|
||||
targets: this.$refs.backdrop,
|
||||
opacity: 0,
|
||||
duration: 200,
|
||||
duration: this.animation ? 200 : 0,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
||||
@ -118,10 +177,33 @@ export default Vue.extend({
|
||||
targets: this.$refs.popover,
|
||||
opacity: 0,
|
||||
scale: 0.5,
|
||||
duration: 200,
|
||||
duration: this.animation ? 200 : 0,
|
||||
easing: 'easeInBack',
|
||||
complete: () => this.destroyDom()
|
||||
complete: () => {
|
||||
this.$emit('closed');
|
||||
this.destroyDom();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
focusUp() {
|
||||
this.focus = this.focus == 0 ? 9 : this.focus < 5 ? (this.focus + 4) : (this.focus - 5);
|
||||
},
|
||||
|
||||
focusDown() {
|
||||
this.focus = this.focus == 9 ? 0 : this.focus >= 5 ? (this.focus - 4) : (this.focus + 5);
|
||||
},
|
||||
|
||||
focusRight() {
|
||||
this.focus = this.focus == 9 ? 0 : (this.focus + 1);
|
||||
},
|
||||
|
||||
focusLeft() {
|
||||
this.focus = this.focus == 0 ? 9 : (this.focus - 1);
|
||||
},
|
||||
|
||||
choose() {
|
||||
this.$refs.buttons.childNodes[this.focus].click();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -207,6 +289,21 @@ root(isDark)
|
||||
width 240px
|
||||
text-align center
|
||||
|
||||
&.showFocus
|
||||
> button:focus
|
||||
z-index 1
|
||||
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
bottom 0
|
||||
left 0
|
||||
border 2px solid rgba($theme-color, 0.3)
|
||||
border-radius 4px
|
||||
|
||||
> button
|
||||
padding 0
|
||||
width 40px
|
||||
|
@ -3,8 +3,7 @@
|
||||
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
|
||||
<p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
|
||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||
<!-- <transition-group v-else tag="div" name="chart"> -->
|
||||
<div>
|
||||
<transition-group v-else tag="div" name="chart">
|
||||
<div v-for="stat in stats" :key="stat.tag">
|
||||
<div class="tag">
|
||||
<router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
|
||||
@ -12,8 +11,7 @@
|
||||
</div>
|
||||
<x-chart class="chart" :src="stat.chart"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- </transition-group> -->
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -100,7 +100,7 @@ export default Vue.extend({
|
||||
|
||||
created() {
|
||||
(this as any).api('chart', {
|
||||
limit: 32
|
||||
limit: 35
|
||||
}).then(chart => {
|
||||
this.chart = chart;
|
||||
});
|
||||
@ -681,6 +681,6 @@ export default Vue.extend({
|
||||
> div
|
||||
> *
|
||||
display block
|
||||
height 320px
|
||||
height 350px
|
||||
|
||||
</style>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy">
|
||||
<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
|
||||
<span slot="header">
|
||||
<span v-html="title" :class="$style.title"></span>
|
||||
<span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}%i18n:@choose-file%)</span>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy">
|
||||
<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
|
||||
<span slot="header">
|
||||
<span v-html="title" :class="$style.title"></span>
|
||||
</span>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout-url="popout">
|
||||
<mk-window ref="window" @closed="destroyDom" width="800px" height="500px" :popout-url="popout">
|
||||
<template slot="header">
|
||||
<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:@used%</p>
|
||||
<span :class="$style.title">%fa:cloud%%i18n:@drive%</span>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mk-window width="400px" height="550px" @closed="$destroy">
|
||||
<mk-window width="400px" height="550px" @closed="destroyDom">
|
||||
<span slot="header" :class="$style.header">
|
||||
<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }}
|
||||
</span>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mk-window width="400px" height="550px" @closed="$destroy">
|
||||
<mk-window width="400px" height="550px" @closed="destroyDom">
|
||||
<span slot="header" :class="$style.header">
|
||||
<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }}
|
||||
</span>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
|
||||
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
|
||||
<span slot="header" :class="$style.header">%fa:gamepad%%i18n:@game%</span>
|
||||
<mk-reversi :class="$style.content" @gamed="g => game = g"/>
|
||||
</mk-window>
|
||||
|
@ -237,6 +237,10 @@ export default Vue.extend({
|
||||
|
||||
warp(date) {
|
||||
(this.$refs.tl as any).warp(date);
|
||||
},
|
||||
|
||||
focus() {
|
||||
(this.$refs.tl as any).focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="$destroy">
|
||||
<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="destroyDom">
|
||||
<span slot="header" :class="$style.header">
|
||||
%fa:i-cursor%{{ title }}
|
||||
</span>
|
||||
|
@ -27,12 +27,13 @@ export default Vue.extend({
|
||||
},
|
||||
raw: {
|
||||
default: false
|
||||
},
|
||||
hide: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hide: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
style(): any {
|
||||
return {
|
||||
@ -89,7 +90,7 @@ export default Vue.extend({
|
||||
text-align center
|
||||
font-size 12px
|
||||
|
||||
> b
|
||||
> *
|
||||
display block
|
||||
|
||||
</style>
|
||||
|
@ -36,12 +36,13 @@ export default Vue.extend({
|
||||
},
|
||||
inlinePlayable: {
|
||||
default: false
|
||||
},
|
||||
hide: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hide: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageStyle(): any {
|
||||
return {
|
||||
@ -79,7 +80,6 @@ export default Vue.extend({
|
||||
justify-content center
|
||||
align-items center
|
||||
font-size 3.5em
|
||||
|
||||
cursor zoom-in
|
||||
overflow hidden
|
||||
background-position center
|
||||
@ -101,5 +101,4 @@ export default Vue.extend({
|
||||
|
||||
> b
|
||||
display block
|
||||
|
||||
</style>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
|
||||
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
|
||||
<span slot="header" :class="$style.header">%fa:comments%%i18n:@title% {{ user | userName }}</span>
|
||||
<mk-messaging-room :user="user" :class="$style.content"/>
|
||||
</mk-window>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mk-window ref="window" width="500px" height="560px" @closed="$destroy">
|
||||
<mk-window ref="window" width="500px" height="560px" @closed="destroyDom">
|
||||
<span slot="header" :class="$style.header">%fa:comments%%i18n:@title%</span>
|
||||
<mk-messaging :class="$style.content" @navigate="navigate"/>
|
||||
</mk-window>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
|
||||
<div class="note" tabindex="-1" v-hotkey="keymap" :title="title">
|
||||
<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
|
||||
<x-sub :note="p.reply"/>
|
||||
</div>
|
||||
@ -40,15 +40,15 @@
|
||||
</div>
|
||||
<footer>
|
||||
<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-else>%fa:reply%</template>
|
||||
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
|
||||
</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>
|
||||
</button>
|
||||
<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@add-reaction%">
|
||||
<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react()" ref="reactButton" title="%i18n:@add-reaction%">
|
||||
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
|
||||
</button>
|
||||
<button @click="menu" ref="menuButton">
|
||||
@ -111,6 +111,27 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'r|left': () => this.reply(true),
|
||||
'a|plus': () => this.react(true),
|
||||
'ctrl+q|ctrl+right': this.renoteDirectly,
|
||||
'q|right': () => this.renote(true),
|
||||
'up|k|shift+tab': this.focusBefore,
|
||||
'down|j|tab': this.focusAfter,
|
||||
'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 {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
@ -220,22 +241,40 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
reply() {
|
||||
reply(viaKeyboard = false) {
|
||||
(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, {
|
||||
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, {
|
||||
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
|
||||
});
|
||||
},
|
||||
|
||||
@ -243,44 +282,23 @@ export default Vue.extend({
|
||||
(this as any).os.new(MkNoteMenu, {
|
||||
source: this.$refs.menuButton,
|
||||
note: this.p
|
||||
});
|
||||
}).$once('closed', this.focus);
|
||||
},
|
||||
|
||||
onKeydown(e) {
|
||||
let shouldBeCancel = true;
|
||||
focus() {
|
||||
this.$el.focus();
|
||||
},
|
||||
|
||||
switch (true) {
|
||||
case e.which == 38: // [↑]
|
||||
case e.which == 74: // [j]
|
||||
case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
|
||||
focus(this.$el, e => e.previousElementSibling);
|
||||
break;
|
||||
blur() {
|
||||
this.$el.blur();
|
||||
},
|
||||
|
||||
case e.which == 40: // [↓]
|
||||
case e.which == 75: // [k]
|
||||
case e.which == 9: // [Tab]
|
||||
focus(this.$el, e => e.nextElementSibling);
|
||||
break;
|
||||
focusBefore() {
|
||||
focus(this.$el, e => e.previousElementSibling);
|
||||
},
|
||||
|
||||
case e.which == 81: // [q]
|
||||
case e.which == 69: // [e]
|
||||
this.renote();
|
||||
break;
|
||||
|
||||
case e.which == 70: // [f]
|
||||
case e.which == 76: // [l]
|
||||
//this.like();
|
||||
break;
|
||||
|
||||
case e.which == 82: // [r]
|
||||
this.reply();
|
||||
break;
|
||||
|
||||
default:
|
||||
shouldBeCancel = false;
|
||||
}
|
||||
|
||||
if (shouldBeCancel) e.preventDefault();
|
||||
focusAfter() {
|
||||
focus(this.$el, e => e.nextElementSibling);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -10,15 +10,15 @@
|
||||
</div>
|
||||
|
||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||
<transition-group 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">
|
||||
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
|
||||
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" ref="note"/>
|
||||
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
||||
<span>%fa:angle-up%{{ note._datetext }}</span>
|
||||
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</transition-group>
|
||||
</component>
|
||||
|
||||
<footer v-if="more">
|
||||
<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
@ -89,7 +89,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
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) {
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="mk-notifications">
|
||||
<div class="notifications" v-if="notifications.length != 0">
|
||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||
<transition-group name="mk-notifications" class="transition" tag="div">
|
||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div">
|
||||
<template v-for="(notification, i) in _notifications">
|
||||
<div class="notification" :class="notification.type" :key="notification.id">
|
||||
<mk-time :time="notification.createdAt"/>
|
||||
@ -96,7 +96,7 @@
|
||||
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</transition-group>
|
||||
</component>
|
||||
</div>
|
||||
<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
|
||||
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mk-window class="mk-post-form-window" ref="window" is-modal @closed="$destroy">
|
||||
<mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed" :animation="animation">
|
||||
<span slot="header" class="mk-post-form-window--header">
|
||||
<span class="icon" v-if="geo">%fa:map-marker-alt%</span>
|
||||
<span v-if="!reply">%i18n:@note%</span>
|
||||
@ -25,7 +25,19 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['reply'],
|
||||
props: {
|
||||
reply: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
|
||||
animation: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
uploadings: [],
|
||||
@ -33,11 +45,13 @@ export default Vue.extend({
|
||||
geo: null
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
(this.$refs.form as any).focus();
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
onChangeUploadings(files) {
|
||||
this.uploadings = files;
|
||||
@ -53,6 +67,10 @@ export default Vue.extend({
|
||||
},
|
||||
onPosted() {
|
||||
(this.$refs.window as any).close();
|
||||
},
|
||||
onWindowClosed() {
|
||||
this.$emit('closed');
|
||||
this.destroyDom();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -49,7 +49,7 @@
|
||||
<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
|
||||
{{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
|
||||
</button>
|
||||
<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
|
||||
<input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
|
||||
<div class="dropzone" v-if="draghover"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="$destroy">
|
||||
<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="destroyDom">
|
||||
<span slot="header">{{ title }}<mk-ellipsis/></span>
|
||||
<div :class="$style.body">
|
||||
<p :class="$style.init" v-if="isNaN(value)">%i18n:@waiting%<mk-ellipsis/></p>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
|
||||
<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
|
||||
<span slot="header">%fa:envelope R% %i18n:@title%</span>
|
||||
|
||||
<div class="slpqaxdoxhvglersgjukmvizkqbmbokc" :data-darkmode="$store.state.device.darkmode">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<mk-window ref="window" is-modal @closed="$destroy">
|
||||
<mk-window ref="window" is-modal @closed="onWindowClosed" :animation="animation">
|
||||
<span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span>
|
||||
<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/>
|
||||
<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/>
|
||||
</mk-window>
|
||||
</template>
|
||||
|
||||
@ -9,26 +9,48 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['note'],
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.onDocumentKeydown);
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
|
||||
animation: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.onDocumentKeydown);
|
||||
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'esc': this.close,
|
||||
'enter': this.post,
|
||||
'q': this.quote,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onDocumentKeydown(e) {
|
||||
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
|
||||
if (e.which == 27) { // Esc
|
||||
(this.$refs.window as any).close();
|
||||
}
|
||||
}
|
||||
post() {
|
||||
(this.$refs.form as any).ok();
|
||||
},
|
||||
quote() {
|
||||
(this.$refs.form as any).onQuote();
|
||||
},
|
||||
close() {
|
||||
(this.$refs.window as any).close();
|
||||
},
|
||||
onPosted() {
|
||||
(this.$refs.window as any).close();
|
||||
},
|
||||
onCanceled() {
|
||||
(this.$refs.window as any).close();
|
||||
},
|
||||
onWindowClosed() {
|
||||
this.$emit('closed');
|
||||
this.destroyDom();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,13 +1,19 @@
|
||||
<template>
|
||||
<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy">
|
||||
<mk-window ref="window" is-modal width="700px" height="550px" @closed="destroyDom">
|
||||
<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span>
|
||||
<mk-settings @done="close"/>
|
||||
<mk-settings :initial-page="initialPage" @done="close"/>
|
||||
</mk-window>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
initialPage: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
(this as any).$refs.window.close();
|
||||
|
65
src/client/app/desktop/views/components/settings.tags.vue
Normal file
65
src/client/app/desktop/views/components/settings.tags.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="vfcitkilproprqtbnpoertpsziierwzi">
|
||||
<div v-for="timeline in timelines" class="timeline">
|
||||
<ui-input v-model="timeline.title" @change="save">
|
||||
<span>%i18n:@title%</span>
|
||||
</ui-input>
|
||||
<ui-textarea :value="timeline.query ? timeline.query.map(tags => tags.join(' ')).join('\n') : ''" @input="onQueryChange(timeline, $event)">
|
||||
<span>%i18n:@query%</span>
|
||||
</ui-textarea>
|
||||
<ui-button class="save" @click="save">%i18n:@save%</ui-button>
|
||||
</div>
|
||||
<ui-button class="add" @click="add">%i18n:@add%</ui-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
timelines: this.$store.state.settings.tagTimelines
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.timelines.push({
|
||||
id: uuid(),
|
||||
title: '',
|
||||
query: ''
|
||||
});
|
||||
|
||||
this.save();
|
||||
},
|
||||
|
||||
save() {
|
||||
this.$store.dispatch('settings/set', { key: 'tagTimelines', value: this.timelines });
|
||||
},
|
||||
|
||||
onQueryChange(timeline, value) {
|
||||
timeline.query = value.split('\n').map(tags => tags.split(' '));
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
root(isDark)
|
||||
> .timeline
|
||||
padding-bottom 16px
|
||||
border-bottom solid 1px rgba(#000, 0.1)
|
||||
|
||||
> .add
|
||||
margin-top 16px
|
||||
|
||||
.vfcitkilproprqtbnpoertpsziierwzi[data-darkmode]
|
||||
root(true)
|
||||
|
||||
.vfcitkilproprqtbnpoertpsziierwzi:not([data-darkmode])
|
||||
root(false)
|
||||
|
||||
</style>
|
@ -5,6 +5,7 @@
|
||||
<p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p>
|
||||
<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p>
|
||||
<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p>
|
||||
<p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'">%fa:hashtag .fw%%i18n:@tags%</p>
|
||||
<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p>
|
||||
<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p>
|
||||
<p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p>
|
||||
@ -60,6 +61,7 @@
|
||||
<button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button>
|
||||
<mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/>
|
||||
<mk-switch v-model="circleIcons" text="%i18n:@circle-icons%"/>
|
||||
<mk-switch v-model="reduceMotion" text="%i18n:common.reduce-motion%"/>
|
||||
<mk-switch v-model="contrastedAcct" text="%i18n:@contrasted-acct%"/>
|
||||
<mk-switch v-model="showFullAcct" text="%i18n:common.show-full-acct%"/>
|
||||
<mk-switch v-model="gradientWindowHeader" text="%i18n:@gradient-window-header%"/>
|
||||
@ -137,6 +139,11 @@
|
||||
<x-drive/>
|
||||
</section>
|
||||
|
||||
<section class="hashtags" v-show="page == 'hashtags'">
|
||||
<h1>%i18n:@tags%</h1>
|
||||
<x-tags/>
|
||||
</section>
|
||||
|
||||
<section class="mute" v-show="page == 'mute'">
|
||||
<h1>%i18n:@mute%</h1>
|
||||
<x-mute/>
|
||||
@ -221,6 +228,7 @@ import XApi from './settings.api.vue';
|
||||
import XApps from './settings.apps.vue';
|
||||
import XSignins from './settings.signins.vue';
|
||||
import XDrive from './settings.drive.vue';
|
||||
import XTags from './settings.tags.vue';
|
||||
import { url, langs, version } from '../../../config';
|
||||
import checkForUpdate from '../../../common/scripts/check-for-update';
|
||||
|
||||
@ -233,11 +241,18 @@ export default Vue.extend({
|
||||
XApi,
|
||||
XApps,
|
||||
XSignins,
|
||||
XDrive
|
||||
XDrive,
|
||||
XTags
|
||||
},
|
||||
props: {
|
||||
initialPage: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: 'profile',
|
||||
page: this.initialPage || 'profile',
|
||||
meta: null,
|
||||
version,
|
||||
langs,
|
||||
@ -246,6 +261,11 @@ export default Vue.extend({
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
reduceMotion: {
|
||||
get() { return this.$store.state.device.reduceMotion; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
|
||||
},
|
||||
|
||||
apiViaStream: {
|
||||
get() { return this.$store.state.device.apiViaStream; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'apiViaStream', value }); }
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
@ -23,6 +24,9 @@ export default Vue.extend({
|
||||
src: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
tagTl: {
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
@ -31,9 +35,17 @@ export default Vue.extend({
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
existMore: false,
|
||||
streamManager: null,
|
||||
connection: null,
|
||||
connectionId: null,
|
||||
date: null
|
||||
date: null,
|
||||
baseQuery: {
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
},
|
||||
query: {},
|
||||
endpoint: null
|
||||
};
|
||||
},
|
||||
|
||||
@ -42,53 +54,109 @@ export default Vue.extend({
|
||||
return this.$store.state.i.followingCount == 0;
|
||||
},
|
||||
|
||||
stream(): any {
|
||||
switch (this.src) {
|
||||
case 'home': return (this as any).os.stream;
|
||||
case 'local': return (this as any).os.streams.localTimelineStream;
|
||||
case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
|
||||
case 'global': return (this as any).os.streams.globalTimelineStream;
|
||||
}
|
||||
},
|
||||
|
||||
endpoint(): string {
|
||||
switch (this.src) {
|
||||
case 'home': return 'notes/timeline';
|
||||
case 'local': return 'notes/local-timeline';
|
||||
case 'hybrid': return 'notes/hybrid-timeline';
|
||||
case 'global': return 'notes/global-timeline';
|
||||
}
|
||||
},
|
||||
|
||||
canFetchMore(): boolean {
|
||||
return !this.moreFetching && !this.fetching && this.existMore;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = this.stream.getConnection();
|
||||
this.connectionId = this.stream.use();
|
||||
const prepend = note => {
|
||||
(this.$refs.timeline as any).prepend(note);
|
||||
};
|
||||
|
||||
this.connection.on('note', this.onNote);
|
||||
if (this.src == 'home') {
|
||||
this.connection.on('follow', this.onChangeFollowing);
|
||||
this.connection.on('unfollow', this.onChangeFollowing);
|
||||
if (this.src == 'tag') {
|
||||
this.endpoint = 'notes/search_by_tag';
|
||||
this.query = {
|
||||
query: this.tagTl.query
|
||||
};
|
||||
this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
|
||||
this.connection.on('note', prepend);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('note', prepend);
|
||||
this.connection.close();
|
||||
});
|
||||
} else if (this.src == 'home') {
|
||||
this.endpoint = 'notes/timeline';
|
||||
const onChangeFollowing = () => {
|
||||
this.fetch();
|
||||
};
|
||||
this.streamManager = (this as any).os.stream;
|
||||
this.connection = this.streamManager.getConnection();
|
||||
this.connectionId = this.streamManager.use();
|
||||
this.connection.on('note', prepend);
|
||||
this.connection.on('follow', onChangeFollowing);
|
||||
this.connection.on('unfollow', onChangeFollowing);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('note', prepend);
|
||||
this.connection.off('follow', onChangeFollowing);
|
||||
this.connection.off('unfollow', onChangeFollowing);
|
||||
this.streamManager.dispose(this.connectionId);
|
||||
});
|
||||
} else if (this.src == 'local') {
|
||||
this.endpoint = 'notes/local-timeline';
|
||||
this.streamManager = (this as any).os.streams.localTimelineStream;
|
||||
this.connection = this.streamManager.getConnection();
|
||||
this.connectionId = this.streamManager.use();
|
||||
this.connection.on('note', prepend);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('note', prepend);
|
||||
this.streamManager.dispose(this.connectionId);
|
||||
});
|
||||
} else if (this.src == 'hybrid') {
|
||||
this.endpoint = 'notes/hybrid-timeline';
|
||||
this.streamManager = (this as any).os.streams.hybridTimelineStream;
|
||||
this.connection = this.streamManager.getConnection();
|
||||
this.connectionId = this.streamManager.use();
|
||||
this.connection.on('note', prepend);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('note', prepend);
|
||||
this.streamManager.dispose(this.connectionId);
|
||||
});
|
||||
} else if (this.src == 'global') {
|
||||
this.endpoint = 'notes/global-timeline';
|
||||
this.streamManager = (this as any).os.streams.globalTimelineStream;
|
||||
this.connection = this.streamManager.getConnection();
|
||||
this.connectionId = this.streamManager.use();
|
||||
this.connection.on('note', prepend);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('note', prepend);
|
||||
this.streamManager.dispose(this.connectionId);
|
||||
});
|
||||
} else if (this.src == 'mentions') {
|
||||
this.endpoint = 'notes/mentions';
|
||||
this.streamManager = (this as any).os.stream;
|
||||
this.connection = this.streamManager.getConnection();
|
||||
this.connectionId = this.streamManager.use();
|
||||
this.connection.on('mention', prepend);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('mention', prepend);
|
||||
this.streamManager.dispose(this.connectionId);
|
||||
});
|
||||
} else if (this.src == 'messages') {
|
||||
this.endpoint = 'notes/mentions';
|
||||
this.query = {
|
||||
visibility: 'specified'
|
||||
};
|
||||
const onNote = note => {
|
||||
if (note.visibility == 'specified') {
|
||||
prepend(note);
|
||||
}
|
||||
};
|
||||
this.streamManager = (this as any).os.stream;
|
||||
this.connection = this.streamManager.getConnection();
|
||||
this.connectionId = this.streamManager.use();
|
||||
this.connection.on('mention', onNote);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('mention', onNote);
|
||||
this.streamManager.dispose(this.connectionId);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', this.onKeydown);
|
||||
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.off('note', this.onNote);
|
||||
if (this.src == 'home') {
|
||||
this.connection.off('follow', this.onChangeFollowing);
|
||||
this.connection.off('unfollow', this.onChangeFollowing);
|
||||
}
|
||||
this.stream.dispose(this.connectionId);
|
||||
|
||||
document.removeEventListener('keydown', this.onKeydown);
|
||||
this.$emit('beforeDestroy');
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -96,13 +164,10 @@ export default Vue.extend({
|
||||
this.fetching = true;
|
||||
|
||||
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
|
||||
(this as any).api(this.endpoint, {
|
||||
(this as any).api(this.endpoint, Object.assign({
|
||||
limit: fetchLimit + 1,
|
||||
untilDate: this.date ? this.date.getTime() : undefined,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
}).then(notes => {
|
||||
untilDate: this.date ? this.date.getTime() : undefined
|
||||
}, this.baseQuery, this.query)).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
this.existMore = true;
|
||||
@ -119,13 +184,10 @@ export default Vue.extend({
|
||||
|
||||
this.moreFetching = true;
|
||||
|
||||
const promise = (this as any).api(this.endpoint, {
|
||||
const promise = (this as any).api(this.endpoint, Object.assign({
|
||||
limit: fetchLimit + 1,
|
||||
untilId: (this.$refs.timeline as any).tail().id,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
});
|
||||
untilId: (this.$refs.timeline as any).tail().id
|
||||
}, this.baseQuery, this.query));
|
||||
|
||||
promise.then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
@ -140,15 +202,6 @@ export default Vue.extend({
|
||||
return promise;
|
||||
},
|
||||
|
||||
onNote(note) {
|
||||
// Prepend a note
|
||||
(this.$refs.timeline as any).prepend(note);
|
||||
},
|
||||
|
||||
onChangeFollowing() {
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
focus() {
|
||||
(this.$refs.timeline as any).focus();
|
||||
},
|
||||
@ -156,14 +209,6 @@ export default Vue.extend({
|
||||
warp(date) {
|
||||
this.date = date;
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
onKeydown(e) {
|
||||
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
|
||||
if (e.which == 84) { // t
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -5,13 +5,22 @@
|
||||
<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
|
||||
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
|
||||
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
|
||||
<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl">%fa:hashtag% {{ tagTl.title }}</span>
|
||||
<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span>
|
||||
<button @click="chooseList" title="%i18n:@list%">%fa:list%</button>
|
||||
<div class="buttons">
|
||||
<button :data-active="src == 'mentions'" @click="src = 'mentions'" title="%i18n:@mentions%">%fa:at%</button>
|
||||
<button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%">%fa:envelope R%</button>
|
||||
<button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button>
|
||||
<button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button>
|
||||
</div>
|
||||
</header>
|
||||
<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/>
|
||||
<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/>
|
||||
<x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
|
||||
<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/>
|
||||
<x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
|
||||
<x-core v-if="src == 'messages'" ref="tl" key="messages" src="messages"/>
|
||||
<x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/>
|
||||
<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
|
||||
</div>
|
||||
</template>
|
||||
@ -19,7 +28,8 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XCore from './timeline.core.vue';
|
||||
import MkUserListsWindow from './user-lists-window.vue';
|
||||
import Menu from '../../../common/views/components/menu.vue';
|
||||
import MkSettingsWindow from './settings-window.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -30,6 +40,7 @@ export default Vue.extend({
|
||||
return {
|
||||
src: 'home',
|
||||
list: null,
|
||||
tagTl: null,
|
||||
enableLocalTimeline: false
|
||||
};
|
||||
},
|
||||
@ -39,8 +50,14 @@ export default Vue.extend({
|
||||
this.saveSrc();
|
||||
},
|
||||
|
||||
list() {
|
||||
list(x) {
|
||||
this.saveSrc();
|
||||
if (x != null) this.tagTl = null;
|
||||
},
|
||||
|
||||
tagTl(x) {
|
||||
this.saveSrc();
|
||||
if (x != null) this.list = null;
|
||||
}
|
||||
},
|
||||
|
||||
@ -53,6 +70,8 @@ export default Vue.extend({
|
||||
this.src = this.$store.state.device.tl.src;
|
||||
if (this.src == 'list') {
|
||||
this.list = this.$store.state.device.tl.arg;
|
||||
} else if (this.src == 'tag') {
|
||||
this.tagTl = this.$store.state.device.tl.arg;
|
||||
}
|
||||
} else if (this.$store.state.i.followingCount == 0) {
|
||||
this.src = 'hybrid';
|
||||
@ -69,20 +88,86 @@ export default Vue.extend({
|
||||
saveSrc() {
|
||||
this.$store.commit('device/setTl', {
|
||||
src: this.src,
|
||||
arg: this.list
|
||||
arg: this.src == 'list' ? this.list : this.tagTl
|
||||
});
|
||||
},
|
||||
|
||||
focus() {
|
||||
(this.$refs.tl as any).focus();
|
||||
},
|
||||
|
||||
warp(date) {
|
||||
(this.$refs.tl as any).warp(date);
|
||||
},
|
||||
|
||||
chooseList() {
|
||||
const w = (this as any).os.new(MkUserListsWindow);
|
||||
w.$once('choosen', list => {
|
||||
this.list = list;
|
||||
this.src = 'list';
|
||||
w.close();
|
||||
async chooseList() {
|
||||
const lists = await (this as any).api('users/lists/list');
|
||||
|
||||
let menu = [{
|
||||
icon: '%fa:plus%',
|
||||
text: '%i18n:@add-list%',
|
||||
action: () => {
|
||||
(this as any).apis.input({
|
||||
title: '%i18n:@list-name%',
|
||||
}).then(async title => {
|
||||
const list = await (this as any).api('users/lists/create', {
|
||||
title
|
||||
});
|
||||
|
||||
this.list = list;
|
||||
this.src = 'list';
|
||||
});
|
||||
}
|
||||
}];
|
||||
|
||||
if (lists.length > 0) {
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
menu = menu.concat(lists.map(list => ({
|
||||
icon: '%fa:list%',
|
||||
text: list.title,
|
||||
action: () => {
|
||||
this.list = list;
|
||||
this.src = 'list';
|
||||
}
|
||||
})));
|
||||
|
||||
this.os.new(Menu, {
|
||||
source: this.$refs.listButton,
|
||||
compact: false,
|
||||
items: menu
|
||||
});
|
||||
},
|
||||
|
||||
chooseTag() {
|
||||
let menu = [{
|
||||
icon: '%fa:plus%',
|
||||
text: '%i18n:@add-tag-timeline%',
|
||||
action: () => {
|
||||
(this as any).os.new(MkSettingsWindow, {
|
||||
initialPage: 'hashtags'
|
||||
});
|
||||
}
|
||||
}];
|
||||
|
||||
if (this.$store.state.settings.tagTimelines.length > 0) {
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
menu = menu.concat(this.$store.state.settings.tagTimelines.map(t => ({
|
||||
icon: '%fa:hashtag%',
|
||||
text: t.title,
|
||||
action: () => {
|
||||
this.tagTl = t;
|
||||
this.src = 'tag';
|
||||
}
|
||||
})));
|
||||
|
||||
this.os.new(Menu, {
|
||||
source: this.$refs.tagButton,
|
||||
compact: false,
|
||||
items: menu
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -104,22 +189,38 @@ root(isDark)
|
||||
border-radius 6px 6px 0 0
|
||||
box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
|
||||
|
||||
> button
|
||||
> .buttons
|
||||
position absolute
|
||||
z-index 2
|
||||
top 0
|
||||
right 0
|
||||
padding 0
|
||||
width 42px
|
||||
font-size 0.9em
|
||||
line-height 42px
|
||||
color isDark ? #9baec8 : #ccc
|
||||
padding-right 8px
|
||||
|
||||
&:hover
|
||||
color isDark ? #b2c1d5 : #aaa
|
||||
> button
|
||||
padding 0 8px
|
||||
font-size 0.9em
|
||||
line-height 42px
|
||||
color isDark ? #9baec8 : #ccc
|
||||
|
||||
&:active
|
||||
color isDark ? #b2c1d5 : #999
|
||||
&:hover
|
||||
color isDark ? #b2c1d5 : #aaa
|
||||
|
||||
&:active
|
||||
color isDark ? #b2c1d5 : #999
|
||||
|
||||
&[data-active]
|
||||
color $theme-color
|
||||
cursor default
|
||||
|
||||
&:before
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
bottom 0
|
||||
left 0
|
||||
width 100%
|
||||
height 2px
|
||||
background $theme-color
|
||||
|
||||
> span
|
||||
display inline-block
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mk-ui" :style="style">
|
||||
<div class="mk-ui" :style="style" v-hotkey.global="keymap">
|
||||
<x-header class="header" v-show="!zenMode"/>
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
@ -16,11 +16,13 @@ export default Vue.extend({
|
||||
components: {
|
||||
XHeader
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
zenMode: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
style(): any {
|
||||
if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {};
|
||||
@ -28,27 +30,24 @@ export default Vue.extend({
|
||||
backgroundColor: this.$store.state.i.wallpaperColor && this.$store.state.i.wallpaperColor.length == 3 ? `rgb(${ this.$store.state.i.wallpaperColor.join(',') })` : null,
|
||||
backgroundImage: `url(${ this.$store.state.i.wallpaperUrl })`
|
||||
};
|
||||
},
|
||||
|
||||
keymap(): any {
|
||||
return {
|
||||
'p': this.post,
|
||||
'n': this.post,
|
||||
'z': this.toggleZenMode
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.onKeydown);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.onKeydown);
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeydown(e) {
|
||||
if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return;
|
||||
post() {
|
||||
(this as any).apis.post();
|
||||
},
|
||||
|
||||
if (e.which == 80 || e.which == 78) { // p or n
|
||||
e.preventDefault();
|
||||
(this as any).apis.post();
|
||||
}
|
||||
|
||||
if (e.which == 90) { // z
|
||||
e.preventDefault();
|
||||
this.zenMode = !this.zenMode;
|
||||
}
|
||||
toggleZenMode() {
|
||||
this.zenMode = !this.zenMode;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
|
||||
<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
|
||||
<span slot="header">%fa:list% %i18n:@title%</span>
|
||||
|
||||
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp" :data-darkmode="$store.state.device.darkmode">
|
||||
|
@ -76,6 +76,11 @@ export default Vue.extend({
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
animation: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
@ -142,7 +147,7 @@ export default Vue.extend({
|
||||
anime({
|
||||
targets: bg,
|
||||
opacity: 1,
|
||||
duration: 100,
|
||||
duration: this.animation ? 100 : 0,
|
||||
easing: 'linear'
|
||||
});
|
||||
}
|
||||
@ -152,7 +157,7 @@ export default Vue.extend({
|
||||
targets: main,
|
||||
opacity: 1,
|
||||
scale: [1.1, 1],
|
||||
duration: 200,
|
||||
duration: this.animation ? 200 : 0,
|
||||
easing: 'easeOutQuad'
|
||||
});
|
||||
|
||||
@ -160,7 +165,7 @@ export default Vue.extend({
|
||||
|
||||
setTimeout(() => {
|
||||
this.$emit('opened');
|
||||
}, 300);
|
||||
}, this.animation ? 300 : 0);
|
||||
},
|
||||
|
||||
close() {
|
||||
@ -174,7 +179,7 @@ export default Vue.extend({
|
||||
anime({
|
||||
targets: bg,
|
||||
opacity: 0,
|
||||
duration: 300,
|
||||
duration: this.animation ? 300 : 0,
|
||||
easing: 'linear'
|
||||
});
|
||||
}
|
||||
@ -185,14 +190,14 @@ export default Vue.extend({
|
||||
targets: main,
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
duration: 300,
|
||||
duration: this.animation ? 300 : 0,
|
||||
easing: [0.5, -0.5, 1, 0.5]
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.destroyDom();
|
||||
this.$emit('closed');
|
||||
}, 300);
|
||||
this.destroyDom();
|
||||
}, this.animation ? 300 : 0);
|
||||
},
|
||||
|
||||
popout() {
|
||||
|
@ -6,6 +6,9 @@
|
||||
<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked"/>
|
||||
<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/>
|
||||
<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/>
|
||||
<x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked"/>
|
||||
<x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked"/>
|
||||
<x-direct-column v-else-if="column.type == 'direct'" :column="column" :is-stacked="isStacked"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@ -13,12 +16,16 @@ import Vue from 'vue';
|
||||
import XTlColumn from './deck.tl-column.vue';
|
||||
import XNotificationsColumn from './deck.notifications-column.vue';
|
||||
import XWidgetsColumn from './deck.widgets-column.vue';
|
||||
import XMentionsColumn from './deck.mentions-column.vue';
|
||||
import XDirectColumn from './deck.direct-column.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XTlColumn,
|
||||
XNotificationsColumn,
|
||||
XWidgetsColumn
|
||||
XWidgetsColumn,
|
||||
XMentionsColumn,
|
||||
XDirectColumn
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -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>
|
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<x-column :name="name" :column="column" :is-stacked="isStacked">
|
||||
<span slot="header">%fa:at%{{ name }}</span>
|
||||
|
||||
<x-mentions/>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XColumn from './deck.column.vue';
|
||||
import XMentions from './deck.mentions.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XColumn,
|
||||
XMentions
|
||||
},
|
||||
|
||||
props: {
|
||||
column: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isStacked: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
name(): string {
|
||||
if (this.column.name) return this.column.name;
|
||||
return '%i18n:common.deck.mentions%';
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
93
src/client/app/desktop/views/pages/deck/deck.mentions.vue
Normal file
93
src/client/app/desktop/views/pages/deck/deck.mentions.vue
Normal file
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<x-notes ref="timeline" :more="existMore ? more : null"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XNotes from './deck.notes.vue';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XNotes
|
||||
},
|
||||
|
||||
props: {
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
existMore: false,
|
||||
connection: null,
|
||||
connectionId: null
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = (this as any).os.stream.getConnection();
|
||||
this.connectionId = (this as any).os.stream.use();
|
||||
|
||||
this.connection.on('mention', this.onNote);
|
||||
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.off('mention', this.onNote);
|
||||
(this as any).os.stream.dispose(this.connectionId);
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
this.fetching = true;
|
||||
|
||||
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
|
||||
(this as any).api('notes/mentions', {
|
||||
limit: fetchLimit + 1,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
this.existMore = true;
|
||||
}
|
||||
res(notes);
|
||||
this.fetching = false;
|
||||
this.$emit('loaded');
|
||||
}, rej);
|
||||
}));
|
||||
},
|
||||
more() {
|
||||
this.moreFetching = true;
|
||||
|
||||
const promise = (this as any).api('notes/mentions', {
|
||||
limit: fetchLimit + 1,
|
||||
untilId: (this.$refs.timeline as any).tail().id,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
});
|
||||
|
||||
promise.then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
} else {
|
||||
this.existMore = false;
|
||||
}
|
||||
notes.forEach(n => (this.$refs.timeline as any).append(n));
|
||||
this.moreFetching = false;
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
onNote(note) {
|
||||
// Prepend a note
|
||||
(this.$refs.timeline as any).prepend(note);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="oxynyeqmfvracxnglgulyqfgqxnxmehl">
|
||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||
<!--<transition-group name="mk-notifications" class="transition notifications">-->
|
||||
<div class="notifications">
|
||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications">
|
||||
<template v-for="(notification, i) in _notifications">
|
||||
<x-notification class="notification" :notification="notification" :key="notification.id"/>
|
||||
<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
|
||||
@ -10,8 +9,7 @@
|
||||
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
<!--</transition-group>-->
|
||||
</component>
|
||||
<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
|
||||
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
|
||||
</button>
|
||||
|
@ -6,6 +6,7 @@
|
||||
<template v-if="column.type == 'hybrid'">%fa:share-alt%</template>
|
||||
<template v-if="column.type == 'global'">%fa:globe%</template>
|
||||
<template v-if="column.type == 'list'">%fa:list%</template>
|
||||
<template v-if="column.type == 'hashtag'">%fa:hashtag%</template>
|
||||
<span>{{ name }}</span>
|
||||
</span>
|
||||
|
||||
@ -14,6 +15,7 @@
|
||||
<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/>
|
||||
</div>
|
||||
<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
|
||||
<x-hashtag-tl v-if="column.type == 'hashtag'" :tag-tl="$store.state.settings.tagTimelines.find(x => x.id == column.tagTlId)" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
|
||||
<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
|
||||
</x-column>
|
||||
</template>
|
||||
@ -23,12 +25,14 @@ import Vue from 'vue';
|
||||
import XColumn from './deck.column.vue';
|
||||
import XTl from './deck.tl.vue';
|
||||
import XListTl from './deck.list-tl.vue';
|
||||
import XHashtagTl from './deck.hashtag-tl.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XColumn,
|
||||
XTl,
|
||||
XListTl
|
||||
XListTl,
|
||||
XHashtagTl
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -65,6 +69,7 @@ export default Vue.extend({
|
||||
case 'hybrid': return '%i18n:common.deck.hybrid%';
|
||||
case 'global': return '%i18n:common.deck.global%';
|
||||
case 'list': return this.column.list.title;
|
||||
case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -138,6 +138,24 @@ export default Vue.extend({
|
||||
type: 'global'
|
||||
});
|
||||
}
|
||||
}, {
|
||||
icon: '%fa:at%',
|
||||
text: '%i18n:common.deck.mentions%',
|
||||
action: () => {
|
||||
this.$store.dispatch('settings/addDeckColumn', {
|
||||
id: uuid(),
|
||||
type: 'mentions'
|
||||
});
|
||||
}
|
||||
}, {
|
||||
icon: '%fa:envelope R%',
|
||||
text: '%i18n:common.deck.direct%',
|
||||
action: () => {
|
||||
this.$store.dispatch('settings/addDeckColumn', {
|
||||
id: uuid(),
|
||||
type: 'direct'
|
||||
});
|
||||
}
|
||||
}, {
|
||||
icon: '%fa:list%',
|
||||
text: '%i18n:common.deck.list%',
|
||||
@ -152,6 +170,20 @@ export default Vue.extend({
|
||||
w.close();
|
||||
});
|
||||
}
|
||||
}, {
|
||||
icon: '%fa:hashtag%',
|
||||
text: '%i18n:common.deck.hashtag%',
|
||||
action: () => {
|
||||
(this as any).apis.input({
|
||||
title: '%i18n:@enter-hashtag-tl-title%'
|
||||
}).then(title => {
|
||||
this.$store.dispatch('settings/addDeckColumn', {
|
||||
id: uuid(),
|
||||
type: 'hashtag',
|
||||
tagTlId: this.$store.state.settings.tagTimelines.find(x => x.title == title).id
|
||||
});
|
||||
});
|
||||
}
|
||||
}, {
|
||||
icon: '%fa:bell R%',
|
||||
text: '%i18n:common.deck.notifications%',
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<mk-ui>
|
||||
<mk-home :mode="mode" @loaded="loaded"/>
|
||||
<mk-home :mode="mode" @loaded="loaded" ref="home" v-hotkey.global="keymap"/>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
@ -15,6 +15,13 @@ export default Vue.extend({
|
||||
default: 'timeline'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
't': this.focus
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.title = (this as any).os.instanceName;
|
||||
|
||||
@ -23,6 +30,9 @@ export default Vue.extend({
|
||||
methods: {
|
||||
loaded() {
|
||||
Progress.done();
|
||||
},
|
||||
focus() {
|
||||
this.$refs.home.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -43,7 +43,7 @@ export default Vue.extend({
|
||||
> .stats
|
||||
display flex
|
||||
justify-content center
|
||||
margin-bottom 16px
|
||||
margin 0 auto 16px auto
|
||||
padding 32px
|
||||
background #fff
|
||||
box-shadow 0 2px 8px rgba(#000, 0.1)
|
||||
@ -60,5 +60,6 @@ export default Vue.extend({
|
||||
font-size 70%
|
||||
|
||||
> div
|
||||
max-width 850px
|
||||
max-width 950px
|
||||
margin 0 auto
|
||||
</style>
|
||||
|
@ -6,7 +6,7 @@
|
||||
<main>
|
||||
<div class="main">
|
||||
<x-header :user="user"/>
|
||||
<mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/>
|
||||
<mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
|
||||
<x-timeline class="timeline" ref="tl" :user="user"/>
|
||||
</div>
|
||||
<div class="side">
|
||||
@ -28,7 +28,6 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import parseAcct from '../../../../../../misc/acct/parse';
|
||||
import getUserName from '../../../../../../misc/get-user-name';
|
||||
import Progress from '../../../../common/scripts/loading';
|
||||
import XHeader from './user.header.vue';
|
||||
import XTimeline from './user.timeline.vue';
|
||||
|
@ -8,6 +8,7 @@ import VueRouter from 'vue-router';
|
||||
import * as TreeView from 'vue-json-tree-view';
|
||||
import VAnimateCss from 'v-animate-css';
|
||||
import VModal from 'vue-js-modal';
|
||||
import VueHotkey from './common/hotkey';
|
||||
|
||||
import App from './app.vue';
|
||||
import checkForUpdate from './common/scripts/check-for-update';
|
||||
@ -19,6 +20,7 @@ Vue.use(VueRouter);
|
||||
Vue.use(TreeView);
|
||||
Vue.use(VAnimateCss);
|
||||
Vue.use(VModal);
|
||||
Vue.use(VueHotkey);
|
||||
|
||||
// Register global directives
|
||||
require('./common/views/directives');
|
||||
|
@ -19,12 +19,13 @@ export default Vue.extend({
|
||||
},
|
||||
raw: {
|
||||
default: false
|
||||
},
|
||||
hide: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hide: true
|
||||
};
|
||||
}
|
||||
computed: {
|
||||
style(): any {
|
||||
let url = `url(${this.image.thumbnailUrl})`;
|
||||
@ -65,7 +66,7 @@ export default Vue.extend({
|
||||
text-align center
|
||||
font-size 12px
|
||||
|
||||
> b
|
||||
> *
|
||||
display block
|
||||
|
||||
</style>
|
||||
|
@ -15,25 +15,28 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
video: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
hide: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hide: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageStyle(): any {
|
||||
return {
|
||||
'background-image': `url(${this.video.url})`
|
||||
};
|
||||
}
|
||||
},})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
@ -14,7 +14,7 @@
|
||||
</div>
|
||||
|
||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||
<transition-group name="mk-notes" class="transition" tag="div">
|
||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div">
|
||||
<template v-for="(note, i) in _notes">
|
||||
<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
|
||||
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
||||
@ -22,7 +22,7 @@
|
||||
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</transition-group>
|
||||
</component>
|
||||
|
||||
<footer v-if="more">
|
||||
<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
|
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="mk-notifications">
|
||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||
<!-- <transition-group name="mk-notifications" class="transition notifications"> -->
|
||||
<div class="transition notifications">
|
||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications">
|
||||
<template v-for="(notification, i) in _notifications">
|
||||
<mk-notification :notification="notification" :key="notification.id"/>
|
||||
<p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date">
|
||||
@ -10,8 +9,7 @@
|
||||
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
<!-- </transition-group> -->
|
||||
</component>
|
||||
|
||||
<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
|
||||
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>
|
||||
|
@ -42,7 +42,7 @@
|
||||
<span v-if="visibility === 'private'">%fa:lock%</span>
|
||||
</button>
|
||||
</footer>
|
||||
<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
|
||||
<input ref="file" class="file" type="file" multiple="multiple" @change="onChangeFile"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hashtags" v-if="recentHashtags.length > 0 && $store.state.settings.suggestRecentHashtags">
|
||||
@ -59,7 +59,7 @@ import MkVisibilityChooser from '../../../common/views/components/visibility-cho
|
||||
import getFace from '../../../common/scripts/get-face';
|
||||
import parse from '../../../../../mfm/parse';
|
||||
import { host } from '../../../config';
|
||||
import { erase } from '../../../../../prelude/array';
|
||||
import { erase, unique } from '../../../../../prelude/array';
|
||||
import { length } from 'stringz';
|
||||
import parseAcct from '../../../../../misc/acct/parse';
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
@ -21,6 +22,9 @@ export default Vue.extend({
|
||||
src: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
tagTl: {
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
@ -29,10 +33,18 @@ export default Vue.extend({
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
existMore: false,
|
||||
streamManager: null,
|
||||
connection: null,
|
||||
connectionId: null,
|
||||
unreadCount: 0,
|
||||
date: null
|
||||
date: null,
|
||||
baseQuery: {
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
},
|
||||
query: {},
|
||||
endpoint: null
|
||||
};
|
||||
},
|
||||
|
||||
@ -41,49 +53,109 @@ export default Vue.extend({
|
||||
return this.$store.state.i.followingCount == 0;
|
||||
},
|
||||
|
||||
stream(): any {
|
||||
switch (this.src) {
|
||||
case 'home': return (this as any).os.stream;
|
||||
case 'local': return (this as any).os.streams.localTimelineStream;
|
||||
case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
|
||||
case 'global': return (this as any).os.streams.globalTimelineStream;
|
||||
}
|
||||
},
|
||||
|
||||
endpoint(): string {
|
||||
switch (this.src) {
|
||||
case 'home': return 'notes/timeline';
|
||||
case 'local': return 'notes/local-timeline';
|
||||
case 'hybrid': return 'notes/hybrid-timeline';
|
||||
case 'global': return 'notes/global-timeline';
|
||||
}
|
||||
},
|
||||
|
||||
canFetchMore(): boolean {
|
||||
return !this.moreFetching && !this.fetching && this.existMore;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = this.stream.getConnection();
|
||||
this.connectionId = this.stream.use();
|
||||
const prepend = note => {
|
||||
(this.$refs.timeline as any).prepend(note);
|
||||
};
|
||||
|
||||
this.connection.on('note', this.onNote);
|
||||
if (this.src == 'home') {
|
||||
this.connection.on('follow', this.onChangeFollowing);
|
||||
this.connection.on('unfollow', this.onChangeFollowing);
|
||||
if (this.src == 'tag') {
|
||||
this.endpoint = 'notes/search_by_tag';
|
||||
this.query = {
|
||||
query: this.tagTl.query
|
||||
};
|
||||
this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
|
||||
this.connection.on('note', prepend);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('note', prepend);
|
||||
this.connection.close();
|
||||
});
|
||||
} else if (this.src == 'home') {
|
||||
this.endpoint = 'notes/timeline';
|
||||
const onChangeFollowing = () => {
|
||||
this.fetch();
|
||||
};
|
||||
this.streamManager = (this as any).os.stream;
|
||||
this.connection = this.streamManager.getConnection();
|
||||
this.connectionId = this.streamManager.use();
|
||||
this.connection.on('note', prepend);
|
||||
this.connection.on('follow', onChangeFollowing);
|
||||
this.connection.on('unfollow', onChangeFollowing);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('note', prepend);
|
||||
this.connection.off('follow', onChangeFollowing);
|
||||
this.connection.off('unfollow', onChangeFollowing);
|
||||
this.streamManager.dispose(this.connectionId);
|
||||
});
|
||||
} else if (this.src == 'local') {
|
||||
this.endpoint = 'notes/local-timeline';
|
||||
this.streamManager = (this as any).os.streams.localTimelineStream;
|
||||
this.connection = this.streamManager.getConnection();
|
||||
this.connectionId = this.streamManager.use();
|
||||
this.connection.on('note', prepend);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('note', prepend);
|
||||
this.streamManager.dispose(this.connectionId);
|
||||
});
|
||||
} else if (this.src == 'hybrid') {
|
||||
this.endpoint = 'notes/hybrid-timeline';
|
||||
this.streamManager = (this as any).os.streams.hybridTimelineStream;
|
||||
this.connection = this.streamManager.getConnection();
|
||||
this.connectionId = this.streamManager.use();
|
||||
this.connection.on('note', prepend);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('note', prepend);
|
||||
this.streamManager.dispose(this.connectionId);
|
||||
});
|
||||
} else if (this.src == 'global') {
|
||||
this.endpoint = 'notes/global-timeline';
|
||||
this.streamManager = (this as any).os.streams.globalTimelineStream;
|
||||
this.connection = this.streamManager.getConnection();
|
||||
this.connectionId = this.streamManager.use();
|
||||
this.connection.on('note', prepend);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('note', prepend);
|
||||
this.streamManager.dispose(this.connectionId);
|
||||
});
|
||||
} else if (this.src == 'mentions') {
|
||||
this.endpoint = 'notes/mentions';
|
||||
this.streamManager = (this as any).os.stream;
|
||||
this.connection = this.streamManager.getConnection();
|
||||
this.connectionId = this.streamManager.use();
|
||||
this.connection.on('mention', prepend);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('mention', prepend);
|
||||
this.streamManager.dispose(this.connectionId);
|
||||
});
|
||||
} else if (this.src == 'messages') {
|
||||
this.endpoint = 'notes/mentions';
|
||||
this.query = {
|
||||
visibility: 'specified'
|
||||
};
|
||||
const onNote = note => {
|
||||
if (note.visibility == 'specified') {
|
||||
prepend(note);
|
||||
}
|
||||
};
|
||||
this.streamManager = (this as any).os.stream;
|
||||
this.connection = this.streamManager.getConnection();
|
||||
this.connectionId = this.streamManager.use();
|
||||
this.connection.on('mention', onNote);
|
||||
this.$once('beforeDestroy', () => {
|
||||
this.connection.off('mention', onNote);
|
||||
this.streamManager.dispose(this.connectionId);
|
||||
});
|
||||
}
|
||||
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.off('note', this.onNote);
|
||||
if (this.src == 'home') {
|
||||
this.connection.off('follow', this.onChangeFollowing);
|
||||
this.connection.off('unfollow', this.onChangeFollowing);
|
||||
}
|
||||
this.stream.dispose(this.connectionId);
|
||||
this.$emit('beforeDestroy');
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -91,13 +163,10 @@ export default Vue.extend({
|
||||
this.fetching = true;
|
||||
|
||||
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
|
||||
(this as any).api(this.endpoint, {
|
||||
(this as any).api(this.endpoint, Object.assign({
|
||||
limit: fetchLimit + 1,
|
||||
untilDate: this.date ? this.date.getTime() : undefined,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
}).then(notes => {
|
||||
untilDate: this.date ? this.date.getTime() : undefined
|
||||
}, this.baseQuery, this.query)).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
this.existMore = true;
|
||||
@ -114,13 +183,10 @@ export default Vue.extend({
|
||||
|
||||
this.moreFetching = true;
|
||||
|
||||
const promise = (this as any).api(this.endpoint, {
|
||||
const promise = (this as any).api(this.endpoint, Object.assign({
|
||||
limit: fetchLimit + 1,
|
||||
untilId: (this.$refs.timeline as any).tail().id,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
});
|
||||
untilId: (this.$refs.timeline as any).tail().id
|
||||
}, this.baseQuery, this.query));
|
||||
|
||||
promise.then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
@ -135,15 +201,6 @@ export default Vue.extend({
|
||||
return promise;
|
||||
},
|
||||
|
||||
onNote(note) {
|
||||
// Prepend a note
|
||||
(this.$refs.timeline as any).prepend(note);
|
||||
},
|
||||
|
||||
onChangeFollowing() {
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
focus() {
|
||||
(this.$refs.timeline as any).focus();
|
||||
},
|
||||
|
@ -6,7 +6,10 @@
|
||||
<span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span>
|
||||
<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span>
|
||||
<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span>
|
||||
<span v-if="src == 'mentions'">%fa:at%%i18n:@mentions%</span>
|
||||
<span v-if="src == 'messages'">%fa:envelope R%%i18n:@messages%</span>
|
||||
<span v-if="src == 'list'">%fa:list%{{ list.title }}</span>
|
||||
<span v-if="src == 'tag'">%fa:hashtag%{{ tagTl.title }}</span>
|
||||
</span>
|
||||
<span style="margin-left:8px">
|
||||
<template v-if="!showNav">%fa:angle-down%</template>
|
||||
@ -21,15 +24,22 @@
|
||||
<main :data-darkmode="$store.state.device.darkmode">
|
||||
<div class="nav" v-if="showNav">
|
||||
<div class="bg" @click="showNav = false"></div>
|
||||
<div class="pointer"></div>
|
||||
<div class="body">
|
||||
<div>
|
||||
<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span>
|
||||
<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
|
||||
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
|
||||
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
|
||||
<div class="hr"></div>
|
||||
<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span>
|
||||
<span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%</span>
|
||||
<template v-if="lists">
|
||||
<div class="hr" v-if="lists.length > 0"></div>
|
||||
<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
|
||||
</template>
|
||||
<div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div>
|
||||
<span v-for="tl in $store.state.settings.tagTimelines" :data-active="src == 'tag' && tagTl == tl" @click="src = 'tag'; tagTl = tl" :key="tl.id">%fa:hashtag% {{ tl.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -39,6 +49,9 @@
|
||||
<x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/>
|
||||
<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
|
||||
<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
|
||||
<x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
|
||||
<x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/>
|
||||
<x-tl v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/>
|
||||
<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
|
||||
</div>
|
||||
</main>
|
||||
@ -60,6 +73,7 @@ export default Vue.extend({
|
||||
src: 'home',
|
||||
list: null,
|
||||
lists: null,
|
||||
tagTl: null,
|
||||
showNav: false,
|
||||
enableLocalTimeline: false
|
||||
};
|
||||
@ -71,9 +85,16 @@ export default Vue.extend({
|
||||
this.saveSrc();
|
||||
},
|
||||
|
||||
list() {
|
||||
list(x) {
|
||||
this.showNav = false;
|
||||
this.saveSrc();
|
||||
if (x != null) this.tagTl = null;
|
||||
},
|
||||
|
||||
tagTl(x) {
|
||||
this.showNav = false;
|
||||
this.saveSrc();
|
||||
if (x != null) this.list = null;
|
||||
},
|
||||
|
||||
showNav(v) {
|
||||
@ -94,6 +115,8 @@ export default Vue.extend({
|
||||
this.src = this.$store.state.device.tl.src;
|
||||
if (this.src == 'list') {
|
||||
this.list = this.$store.state.device.tl.arg;
|
||||
} else if (this.src == 'tag') {
|
||||
this.tagTl = this.$store.state.device.tl.arg;
|
||||
}
|
||||
} else if (this.$store.state.i.followingCount == 0) {
|
||||
this.src = 'hybrid';
|
||||
@ -118,7 +141,7 @@ export default Vue.extend({
|
||||
saveSrc() {
|
||||
this.$store.commit('device/setTl', {
|
||||
src: this.src,
|
||||
arg: this.list
|
||||
arg: this.src == 'list' ? this.list : this.tagTl
|
||||
});
|
||||
},
|
||||
|
||||
@ -134,6 +157,26 @@ export default Vue.extend({
|
||||
|
||||
root(isDark)
|
||||
> .nav
|
||||
> .pointer
|
||||
position fixed
|
||||
z-index 10002
|
||||
top 56px
|
||||
left 0
|
||||
right 0
|
||||
|
||||
$size = 16px
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
top -($size * 2)
|
||||
left s('calc(50% - %s)', $size)
|
||||
border-top solid $size transparent
|
||||
border-left solid $size transparent
|
||||
border-right solid $size transparent
|
||||
border-bottom solid $size isDark ? #272f3a : #fff
|
||||
|
||||
> .bg
|
||||
position fixed
|
||||
z-index 10000
|
||||
@ -150,28 +193,22 @@ root(isDark)
|
||||
left 0
|
||||
right 0
|
||||
width 300px
|
||||
max-height calc(100% - 70px)
|
||||
margin 0 auto
|
||||
overflow auto
|
||||
-webkit-overflow-scrolling touch
|
||||
background isDark ? #272f3a : #fff
|
||||
border-radius 8px
|
||||
box-shadow 0 0 16px rgba(#000, 0.1)
|
||||
|
||||
$balloon-size = 16px
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
top -($balloon-size * 2) + 1.5px
|
||||
left s('calc(50% - %s)', $balloon-size)
|
||||
border-top solid $balloon-size transparent
|
||||
border-left solid $balloon-size transparent
|
||||
border-right solid $balloon-size transparent
|
||||
border-bottom solid $balloon-size isDark ? #272f3a : #fff
|
||||
|
||||
> div
|
||||
padding 8px 0
|
||||
|
||||
> *
|
||||
> .hr
|
||||
margin 8px 0
|
||||
border-top solid 1px isDark ? rgba(#000, 0.3) : rgba(#000, 0.1)
|
||||
|
||||
> *:not(.hr)
|
||||
display block
|
||||
padding 8px 16px
|
||||
color isDark ? #cdd0d8 : #666
|
||||
|
@ -13,6 +13,7 @@
|
||||
<section>
|
||||
<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
|
||||
<ui-switch v-model="circleIcons">%i18n:@circle-icons%</ui-switch>
|
||||
<ui-switch v-model="reduceMotion">%i18n:common.reduce-motion% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
|
||||
<ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch>
|
||||
<ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch>
|
||||
<ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch>
|
||||
@ -168,6 +169,11 @@ export default Vue.extend({
|
||||
set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
|
||||
},
|
||||
|
||||
reduceMotion: {
|
||||
get() { return this.$store.state.device.reduceMotion; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
|
||||
},
|
||||
|
||||
alwaysShowNsfw: {
|
||||
get() { return this.$store.state.device.alwaysShowNsfw; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); }
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="root home">
|
||||
<mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/>
|
||||
<mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
|
||||
<section class="recent-notes">
|
||||
<h2>%fa:R comments%%i18n:@recent-notes%</h2>
|
||||
<div>
|
||||
|
@ -10,6 +10,7 @@ const defaultSettings = {
|
||||
home: null,
|
||||
mobileHome: [],
|
||||
deck: null,
|
||||
tagTimelines: [],
|
||||
fetchOnScroll: true,
|
||||
showMaps: true,
|
||||
showPostFormOnTopOfTl: false,
|
||||
@ -38,6 +39,7 @@ const defaultSettings = {
|
||||
};
|
||||
|
||||
const defaultDeviceSettings = {
|
||||
reduceMotion: false,
|
||||
apiViaStream: true,
|
||||
autoPopout: false,
|
||||
darkmode: false,
|
||||
|
@ -101,15 +101,15 @@ props:
|
||||
ja-JP: "投稿の数"
|
||||
en-US: "The number of the notes of this user"
|
||||
|
||||
pinnedNote:
|
||||
type: "entity(Note)"
|
||||
pinnedNotes:
|
||||
type: "entity(Note)[]"
|
||||
optional: true
|
||||
desc:
|
||||
ja-JP: "ピン留めされた投稿"
|
||||
en-US: "The pinned note of this user"
|
||||
|
||||
pinnedNoteId:
|
||||
type: "id(Note)"
|
||||
pinnedNoteIds:
|
||||
type: "id(Note)[]"
|
||||
optional: true
|
||||
desc:
|
||||
ja-JP: "ピン留めされた投稿のID"
|
||||
|
@ -9,7 +9,7 @@ html(lang= lang)
|
||||
link(rel="stylesheet" href="/docs/assets/style.css")
|
||||
link(rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css")
|
||||
script(src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js")
|
||||
link(rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous")
|
||||
link(rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous")
|
||||
block meta
|
||||
|
||||
body
|
||||
|
@ -82,8 +82,12 @@ const handlers: { [key: string]: (window: any, token: any, mentionedRemoteUsers:
|
||||
|
||||
text({ document }, { content }) {
|
||||
const nodes = (content as string).split('\n').map(x => document.createTextNode(x));
|
||||
for (const x of intersperse(document.createElement('br'), nodes)) {
|
||||
document.body.appendChild(x);
|
||||
for (const x of intersperse('br', nodes)) {
|
||||
if (x === 'br') {
|
||||
document.body.appendChild(document.createElement('br'));
|
||||
} else {
|
||||
document.body.appendChild(x);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -9,9 +9,9 @@ export type TextElementHashtag = {
|
||||
};
|
||||
|
||||
export default function(text: string, i: number) {
|
||||
if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null;
|
||||
if (!(/^\s#[^\s\.,]+/.test(text) || (i == 0 && /^#[^\s\.,]+/.test(text)))) return null;
|
||||
const isHead = text.startsWith('#');
|
||||
const hashtag = text.match(/^\s?#[^\s]+/)[0];
|
||||
const hashtag = text.match(/^\s?#[^\s\.,]+/)[0];
|
||||
const res: any[] = !isHead ? [{
|
||||
type: 'text',
|
||||
content: text[0]
|
||||
|
@ -2,12 +2,12 @@
|
||||
* Replace fontawesome symbols
|
||||
*/
|
||||
|
||||
import * as fontawesome from '@fortawesome/fontawesome';
|
||||
import regular from '@fortawesome/fontawesome-free-regular';
|
||||
import solid from '@fortawesome/fontawesome-free-solid';
|
||||
import brands from '@fortawesome/fontawesome-free-brands';
|
||||
import * as fontawesome from '@fortawesome/fontawesome-svg-core';
|
||||
import { far } from '@fortawesome/free-regular-svg-icons';
|
||||
import { fas } from '@fortawesome/free-solid-svg-icons';
|
||||
import { fab } from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
fontawesome.library.add(regular, solid, brands);
|
||||
fontawesome.library.add(far, fas, fab);
|
||||
|
||||
export const pattern = /%fa:(.+?)%/g;
|
||||
|
||||
|
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;
|
||||
}
|
@ -17,6 +17,8 @@ import Following from './following';
|
||||
const Note = db.get<INote>('notes');
|
||||
Note.createIndex('uri', { sparse: true, unique: true });
|
||||
Note.createIndex('userId');
|
||||
Note.createIndex('mentions');
|
||||
Note.createIndex('visibleUserIds');
|
||||
Note.createIndex('tagsLower');
|
||||
Note.createIndex('_files.contentType');
|
||||
Note.createIndex({
|
||||
@ -24,6 +26,21 @@ Note.createIndex({
|
||||
});
|
||||
export default Note;
|
||||
|
||||
// 後方互換性のため
|
||||
Note.findOne({
|
||||
fileIds: { $exists: true }
|
||||
}).then(n => {
|
||||
if (n == null) {
|
||||
Note.update({}, {
|
||||
$rename: {
|
||||
mediaIds: 'fileIds'
|
||||
}
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export function isValidText(text: string): boolean {
|
||||
return length(text.trim()) <= 1000 && text.trim() != '';
|
||||
}
|
||||
@ -179,7 +196,7 @@ export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => {
|
||||
hide = false;
|
||||
} else {
|
||||
// 指定されているかどうか
|
||||
const specified = packedNote.visibleUserIds.some((id: mongo.ObjectID) => id.equals(meId));
|
||||
const specified = packedNote.visibleUserIds.some((id: any) => meId.equals(id));
|
||||
|
||||
if (specified) {
|
||||
hide = false;
|
||||
|
@ -35,6 +35,28 @@ User.createIndex('uri', { sparse: true, unique: true });
|
||||
|
||||
export default User;
|
||||
|
||||
// 後方互換性のため
|
||||
User.findOne({
|
||||
pinnedNoteId: { $exists: true }
|
||||
}).then(async x => {
|
||||
if (x == null) return;
|
||||
|
||||
const users = await User.find({
|
||||
pinnedNoteId: { $exists: true }
|
||||
});
|
||||
|
||||
users.forEach(u => {
|
||||
User.update({ _id: u._id }, {
|
||||
$set: {
|
||||
pinnedNoteIds: [(u as any).pinnedNoteId]
|
||||
},
|
||||
$unset: {
|
||||
pinnedNoteId: ''
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type IUserBase = {
|
||||
_id: mongo.ObjectID;
|
||||
createdAt: Date;
|
||||
@ -53,7 +75,7 @@ type IUserBase = {
|
||||
wallpaperUrl?: string;
|
||||
data: any;
|
||||
description: string;
|
||||
pinnedNoteId: mongo.ObjectID;
|
||||
pinnedNoteIds: mongo.ObjectID[];
|
||||
|
||||
/**
|
||||
* 凍結されているか否か
|
||||
@ -464,11 +486,11 @@ export const pack = (
|
||||
}
|
||||
|
||||
if (opts.detail) {
|
||||
if (_user.pinnedNoteId) {
|
||||
// Populate pinned note
|
||||
_user.pinnedNote = packNote(_user.pinnedNoteId, meId, {
|
||||
if (_user.pinnedNoteIds) {
|
||||
// Populate pinned notes
|
||||
_user.pinnedNotes = Promise.all(_user.pinnedNoteIds.map((id: mongo.ObjectId) => packNote(id, meId, {
|
||||
detail: true
|
||||
});
|
||||
})));
|
||||
}
|
||||
|
||||
if (meId && !meId.equals(_user.id)) {
|
||||
|
@ -1,22 +1,10 @@
|
||||
import { INote } from '../../../models/note';
|
||||
import toHtml from '../../../mfm/html';
|
||||
import parse from '../../../mfm/parse';
|
||||
import config from '../../../config';
|
||||
|
||||
export default function(note: INote) {
|
||||
let html = toHtml(parse(note.text), note.mentionedRemoteUsers);
|
||||
if (html == null) html = '';
|
||||
|
||||
if (note.poll != null) {
|
||||
const url = `${config.url}/notes/${note._id}`;
|
||||
// TODO: i18n
|
||||
html += `<p><a href="${url}">【Misskeyで投票を見る】</a></p>`;
|
||||
}
|
||||
|
||||
if (note.renoteId != null) {
|
||||
const url = `${config.url}/notes/${note.renoteId}`;
|
||||
html += `<p>RE: <a href="${url}">${url}</a></p>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
9
src/remote/activitypub/renderer/add.ts
Normal file
9
src/remote/activitypub/renderer/add.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import config from '../../../config';
|
||||
import { ILocalUser } from '../../../models/user';
|
||||
|
||||
export default (user: ILocalUser, target: any, object: any) => ({
|
||||
type: 'Add',
|
||||
actor: `${config.url}/users/${user._id}`,
|
||||
target,
|
||||
object
|
||||
});
|
@ -6,6 +6,7 @@ import DriveFile, { IDriveFile } from '../../../models/drive-file';
|
||||
import Note, { INote } from '../../../models/note';
|
||||
import User from '../../../models/user';
|
||||
import toHtml from '../misc/get-note-html';
|
||||
import parseMfm from '../../../mfm/parse';
|
||||
|
||||
export default async function renderNote(note: INote, dive = true): Promise<any> {
|
||||
const promisedFiles: Promise<IDriveFile[]> = note.fileIds
|
||||
@ -81,13 +82,39 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
|
||||
|
||||
const files = await promisedFiles;
|
||||
|
||||
let text = note.text;
|
||||
|
||||
if (note.poll != null) {
|
||||
if (text == null) text = '';
|
||||
const url = `${config.url}/notes/${note._id}`;
|
||||
// TODO: i18n
|
||||
text += `\n\n[投票を見る](${url})`;
|
||||
}
|
||||
|
||||
if (note.renoteId != null) {
|
||||
if (text == null) text = '';
|
||||
const url = `${config.url}/notes/${note.renoteId}`;
|
||||
text += `\n\nRE: ${url}`;
|
||||
}
|
||||
|
||||
// 省略されたメンションのホストを復元する
|
||||
if (text != null) {
|
||||
text = parseMfm(text).map(x => {
|
||||
if (x.type == 'mention' && x.host == null) {
|
||||
return `${x.content}@${config.host}`;
|
||||
} else {
|
||||
return x.content;
|
||||
}
|
||||
}).join('');
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${config.url}/notes/${note._id}`,
|
||||
type: 'Note',
|
||||
attributedTo,
|
||||
summary: note.cw,
|
||||
content: toHtml(note),
|
||||
_misskey_content_: note.text,
|
||||
content: toHtml(Object.assign({}, note, { text })),
|
||||
_misskey_content: text,
|
||||
published: note.createdAt.toISOString(),
|
||||
to,
|
||||
cc,
|
||||
|
@ -4,8 +4,9 @@
|
||||
* @param totalItems Total number of items
|
||||
* @param first URL of first page (optional)
|
||||
* @param last URL of last page (optional)
|
||||
* @param orderedItems attached objects (optional)
|
||||
*/
|
||||
export default function(id: string, totalItems: any, first: string, last: string) {
|
||||
export default function(id: string, totalItems: any, first?: string, last?: string, orderedItems?: object) {
|
||||
const page: any = {
|
||||
id,
|
||||
type: 'OrderedCollection',
|
||||
@ -14,6 +15,7 @@ export default function(id: string, totalItems: any, first: string, last: string
|
||||
|
||||
if (first) page.first = first;
|
||||
if (last) page.last = last;
|
||||
if (orderedItems) page.orderedItems = orderedItems;
|
||||
|
||||
return page;
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ export default async (user: ILocalUser) => {
|
||||
outbox: `${id}/outbox`,
|
||||
followers: `${id}/followers`,
|
||||
following: `${id}/following`,
|
||||
featured: `${id}/collections/featured`,
|
||||
sharedInbox: `${config.url}/inbox`,
|
||||
url: `${config.url}/@${user.username}`,
|
||||
preferredUsername: user.username,
|
||||
|
9
src/remote/activitypub/renderer/remove.ts
Normal file
9
src/remote/activitypub/renderer/remove.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import config from '../../../config';
|
||||
import { ILocalUser } from '../../../models/user';
|
||||
|
||||
export default (user: ILocalUser, target: any, object: any) => ({
|
||||
type: 'Remove',
|
||||
actor: `${config.url}/users/${user._id}`,
|
||||
target,
|
||||
object
|
||||
});
|
@ -53,6 +53,7 @@ export interface IPerson extends IObject {
|
||||
publicKey: any;
|
||||
followers: any;
|
||||
following: any;
|
||||
featured?: any;
|
||||
outbox: any;
|
||||
endpoints: string[];
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import renderPerson from '../remote/activitypub/renderer/person';
|
||||
import Outbox, { packActivity } from './activitypub/outbox';
|
||||
import Followers from './activitypub/followers';
|
||||
import Following from './activitypub/following';
|
||||
import Featured from './activitypub/featured';
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
@ -102,6 +103,9 @@ router.get('/users/:user/followers', Followers);
|
||||
// following
|
||||
router.get('/users/:user/following', Following);
|
||||
|
||||
// featured
|
||||
router.get('/users/:user/collections/featured', Featured);
|
||||
|
||||
// publickey
|
||||
router.get('/users/:user/publickey', async ctx => {
|
||||
const userId = new mongo.ObjectID(ctx.params.user);
|
||||
|
38
src/server/activitypub/featured.ts
Normal file
38
src/server/activitypub/featured.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import * as mongo from 'mongodb';
|
||||
import * as Router from 'koa-router';
|
||||
import config from '../../config';
|
||||
import User from '../../models/user';
|
||||
import pack from '../../remote/activitypub/renderer';
|
||||
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
|
||||
import { setResponseType } from '../activitypub';
|
||||
import Note from '../../models/note';
|
||||
import renderNote from '../../remote/activitypub/renderer/note';
|
||||
|
||||
export default async (ctx: Router.IRouterContext) => {
|
||||
const userId = new mongo.ObjectID(ctx.params.user);
|
||||
|
||||
// Verify user
|
||||
const user = await User.findOne({
|
||||
_id: userId,
|
||||
host: null
|
||||
});
|
||||
|
||||
if (user === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const pinnedNoteIds = user.pinnedNoteIds || [];
|
||||
|
||||
const pinnedNotes = await Promise.all(pinnedNoteIds.map(id => Note.findOne({ _id: id })));
|
||||
|
||||
const renderedNotes = await Promise.all(pinnedNotes.map(note => renderNote(note)));
|
||||
|
||||
const rendered = renderOrderedCollection(
|
||||
`${config.url}/users/${userId}/collections/featured`,
|
||||
renderedNotes.length, null, null, renderedNotes
|
||||
);
|
||||
|
||||
ctx.body = pack(rendered);
|
||||
setResponseType(ctx);
|
||||
};
|
@ -1,7 +1,9 @@
|
||||
import * as mongo from 'mongodb';
|
||||
import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
|
||||
import User, { ILocalUser } from '../../../../models/user';
|
||||
import Note from '../../../../models/note';
|
||||
import { pack } from '../../../../models/user';
|
||||
import { deliverPinnedChange } from '../../../../services/i/pin';
|
||||
|
||||
/**
|
||||
* Pin note
|
||||
@ -21,9 +23,25 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
|
||||
return rej('note not found');
|
||||
}
|
||||
|
||||
let addedId: mongo.ObjectID;
|
||||
let removedId: mongo.ObjectID;
|
||||
|
||||
const pinnedNoteIds = user.pinnedNoteIds || [];
|
||||
|
||||
if (pinnedNoteIds.some(id => id.equals(note._id))) {
|
||||
return rej('already exists');
|
||||
}
|
||||
|
||||
pinnedNoteIds.unshift(note._id);
|
||||
addedId = note._id;
|
||||
|
||||
if (pinnedNoteIds.length > 5) {
|
||||
removedId = pinnedNoteIds.pop();
|
||||
}
|
||||
|
||||
await User.update(user._id, {
|
||||
$set: {
|
||||
pinnedNoteId: note._id
|
||||
pinnedNoteIds: pinnedNoteIds
|
||||
}
|
||||
});
|
||||
|
||||
@ -32,6 +50,9 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
|
||||
detail: true
|
||||
});
|
||||
|
||||
// Send Add/Remove to followers
|
||||
deliverPinnedChange(user._id, removedId, addedId);
|
||||
|
||||
// Send response
|
||||
res(iObj);
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import Note from '../../../../models/note';
|
||||
import { getFriendIds } from '../../common/get-friends';
|
||||
import { pack } from '../../../../models/note';
|
||||
import { ILocalUser } from '../../../../models/user';
|
||||
import getParams from '../../get-params';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
@ -10,42 +11,55 @@ export const meta = {
|
||||
'en-US': 'Get mentions of myself.'
|
||||
},
|
||||
|
||||
requireCredential: true
|
||||
requireCredential: true,
|
||||
|
||||
params: {
|
||||
following: $.bool.optional.note({
|
||||
default: false
|
||||
}),
|
||||
|
||||
limit: $.num.optional.range(1, 100).note({
|
||||
default: 10
|
||||
}),
|
||||
|
||||
sinceId: $.type(ID).optional.note({
|
||||
}),
|
||||
|
||||
untilId: $.type(ID).optional.note({
|
||||
}),
|
||||
|
||||
visibility: $.str.optional.note({
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
|
||||
// Get 'following' parameter
|
||||
const [following = false, followingError] =
|
||||
$.bool.optional.get(params.following);
|
||||
if (followingError) return rej('invalid following param');
|
||||
|
||||
// Get 'limit' parameter
|
||||
const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
|
||||
if (limitErr) return rej('invalid limit param');
|
||||
|
||||
// Get 'sinceId' parameter
|
||||
const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
|
||||
if (sinceIdErr) return rej('invalid sinceId param');
|
||||
|
||||
// Get 'untilId' parameter
|
||||
const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
|
||||
if (untilIdErr) return rej('invalid untilId param');
|
||||
const [ps, psErr] = getParams(meta, params);
|
||||
if (psErr) throw psErr;
|
||||
|
||||
// Check if both of sinceId and untilId is specified
|
||||
if (sinceId && untilId) {
|
||||
if (ps.sinceId && ps.untilId) {
|
||||
return rej('cannot set sinceId and untilId');
|
||||
}
|
||||
|
||||
// Construct query
|
||||
const query = {
|
||||
mentions: user._id
|
||||
$or: [{
|
||||
mentions: user._id
|
||||
}, {
|
||||
visibleUserIds: user._id
|
||||
}]
|
||||
} as any;
|
||||
|
||||
const sort = {
|
||||
_id: -1
|
||||
};
|
||||
|
||||
if (following) {
|
||||
if (ps.visibility) {
|
||||
query.visibility = ps.visibility;
|
||||
}
|
||||
|
||||
if (ps.following) {
|
||||
const followingIds = await getFriendIds(user._id);
|
||||
|
||||
query.userId = {
|
||||
@ -53,26 +67,24 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
|
||||
};
|
||||
}
|
||||
|
||||
if (sinceId) {
|
||||
if (ps.sinceId) {
|
||||
sort._id = 1;
|
||||
query._id = {
|
||||
$gt: sinceId
|
||||
$gt: ps.sinceId
|
||||
};
|
||||
} else if (untilId) {
|
||||
} else if (ps.untilId) {
|
||||
query._id = {
|
||||
$lt: untilId
|
||||
$lt: ps.untilId
|
||||
};
|
||||
}
|
||||
|
||||
// Issue query
|
||||
const mentions = await Note
|
||||
.find(query, {
|
||||
limit: limit,
|
||||
limit: ps.limit,
|
||||
sort: sort
|
||||
});
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(mentions.map(async mention =>
|
||||
await pack(mention, user)
|
||||
)));
|
||||
res(await Promise.all(mentions.map(mention => pack(mention, user))));
|
||||
});
|
||||
|
@ -13,12 +13,18 @@ export const meta = {
|
||||
},
|
||||
|
||||
params: {
|
||||
tag: $.str.note({
|
||||
tag: $.str.optional.note({
|
||||
desc: {
|
||||
'ja-JP': 'タグ'
|
||||
}
|
||||
}),
|
||||
|
||||
query: $.arr($.arr($.str)).optional.note({
|
||||
desc: {
|
||||
'ja-JP': 'クエリ'
|
||||
}
|
||||
}),
|
||||
|
||||
includeUserIds: $.arr($.type(ID)).optional.note({
|
||||
default: []
|
||||
}),
|
||||
@ -59,11 +65,9 @@ export const meta = {
|
||||
}
|
||||
}),
|
||||
|
||||
withFiles: $.bool.optional.nullable.note({
|
||||
default: null,
|
||||
|
||||
withFiles: $.bool.optional.note({
|
||||
desc: {
|
||||
'ja-JP': 'ファイルが添付された投稿に限定するか否か'
|
||||
'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
|
||||
}
|
||||
}),
|
||||
|
||||
@ -83,6 +87,12 @@ export const meta = {
|
||||
}
|
||||
}),
|
||||
|
||||
untilId: $.type(ID).optional.note({
|
||||
desc: {
|
||||
'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
|
||||
}
|
||||
}),
|
||||
|
||||
sinceDate: $.num.optional.note({
|
||||
}),
|
||||
|
||||
@ -126,8 +136,14 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
|
||||
}
|
||||
|
||||
const q: any = {
|
||||
$and: [{
|
||||
$and: [ps.tag ? {
|
||||
tagsLower: ps.tag.toLowerCase()
|
||||
} : {
|
||||
$or: ps.query.map(tags => ({
|
||||
$and: tags.map(t => ({
|
||||
tagsLower: t.toLowerCase()
|
||||
}))
|
||||
}))
|
||||
}],
|
||||
deletedAt: { $exists: false }
|
||||
};
|
||||
@ -281,25 +297,10 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
|
||||
|
||||
const withFiles = ps.withFiles != null ? ps.withFiles : ps.media;
|
||||
|
||||
if (withFiles != null) {
|
||||
if (withFiles) {
|
||||
push({
|
||||
fileIds: {
|
||||
$exists: true,
|
||||
$ne: null
|
||||
}
|
||||
});
|
||||
} else {
|
||||
push({
|
||||
$or: [{
|
||||
fileIds: {
|
||||
$exists: false
|
||||
}
|
||||
}, {
|
||||
fileIds: null
|
||||
}]
|
||||
});
|
||||
}
|
||||
if (withFiles) {
|
||||
push({
|
||||
fileIds: { $exists: true, $ne: [] }
|
||||
});
|
||||
}
|
||||
|
||||
if (ps.poll != null) {
|
||||
@ -323,6 +324,14 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.untilId) {
|
||||
push({
|
||||
_id: {
|
||||
$lt: ps.untilId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (ps.sinceDate) {
|
||||
push({
|
||||
createdAt: {
|
||||
|
@ -3,6 +3,7 @@ import Xev from 'xev';
|
||||
|
||||
import { IUser } from '../../../models/user';
|
||||
import Mute from '../../../models/mute';
|
||||
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
|
||||
|
||||
export default async function(
|
||||
request: websocket.request,
|
||||
@ -15,17 +16,8 @@ export default async function(
|
||||
|
||||
// Subscribe stream
|
||||
subscriber.on('global-timeline', async note => {
|
||||
//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (mutedUserIds.indexOf(note.userId) != -1) {
|
||||
return;
|
||||
}
|
||||
if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
|
||||
return;
|
||||
}
|
||||
if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
|
||||
return;
|
||||
}
|
||||
//#endregion
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (shouldMuteThisNote(note, mutedUserIds)) return;
|
||||
|
||||
connection.send(JSON.stringify({
|
||||
type: 'note',
|
||||
|
40
src/server/api/stream/hashtag.ts
Normal file
40
src/server/api/stream/hashtag.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import * as websocket from 'websocket';
|
||||
import Xev from 'xev';
|
||||
|
||||
import { IUser } from '../../../models/user';
|
||||
import Mute from '../../../models/mute';
|
||||
import { pack } from '../../../models/note';
|
||||
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
|
||||
|
||||
export default async function(
|
||||
request: websocket.request,
|
||||
connection: websocket.connection,
|
||||
subscriber: Xev,
|
||||
user?: IUser
|
||||
) {
|
||||
const mute = user ? await Mute.find({ muterId: user._id }) : null;
|
||||
const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
|
||||
|
||||
const q: Array<string[]> = JSON.parse((request.resourceURL.query as any).q);
|
||||
|
||||
// Subscribe stream
|
||||
subscriber.on('hashtag', async note => {
|
||||
const matched = q.some(tags => tags.every(tag => note.tags.map((t: string) => t.toLowerCase()).includes(tag.toLowerCase())));
|
||||
if (!matched) return;
|
||||
|
||||
// Renoteなら再pack
|
||||
if (note.renoteId != null) {
|
||||
note.renote = await pack(note.renoteId, user, {
|
||||
detail: true
|
||||
});
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (shouldMuteThisNote(note, mutedUserIds)) return;
|
||||
|
||||
connection.send(JSON.stringify({
|
||||
type: 'note',
|
||||
body: note
|
||||
}));
|
||||
});
|
||||
}
|
@ -8,6 +8,7 @@ import { pack as packNote, pack } from '../../../models/note';
|
||||
import readNotification from '../common/read-notification';
|
||||
import call from '../call';
|
||||
import { IApp } from '../../../models/app';
|
||||
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
|
||||
|
||||
const log = debug('misskey');
|
||||
|
||||
@ -45,15 +46,7 @@ export default async function(
|
||||
|
||||
//#region 流れてきたメッセージがミュートしているユーザーが関わるものだったら無視する
|
||||
if (x.type == 'note') {
|
||||
if (mutedUserIds.includes(x.body.userId)) {
|
||||
return;
|
||||
}
|
||||
if (x.body.reply != null && mutedUserIds.includes(x.body.reply.userId)) {
|
||||
return;
|
||||
}
|
||||
if (x.body.renote != null && mutedUserIds.includes(x.body.renote.userId)) {
|
||||
return;
|
||||
}
|
||||
if (shouldMuteThisNote(x.body, mutedUserIds)) return;
|
||||
} else if (x.type == 'notification') {
|
||||
if (mutedUserIds.includes(x.body.userId)) {
|
||||
return;
|
||||
|
@ -4,6 +4,7 @@ import Xev from 'xev';
|
||||
import { IUser } from '../../../models/user';
|
||||
import Mute from '../../../models/mute';
|
||||
import { pack } from '../../../models/note';
|
||||
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
|
||||
|
||||
export default async function(
|
||||
request: websocket.request,
|
||||
@ -26,17 +27,8 @@ export default async function(
|
||||
});
|
||||
}
|
||||
|
||||
//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (mutedUserIds.indexOf(note.userId) != -1) {
|
||||
return;
|
||||
}
|
||||
if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
|
||||
return;
|
||||
}
|
||||
if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
|
||||
return;
|
||||
}
|
||||
//#endregion
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (shouldMuteThisNote(note, mutedUserIds)) return;
|
||||
|
||||
connection.send(JSON.stringify({
|
||||
type: 'note',
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user