Compare commits
109 Commits
Author | SHA1 | Date | |
---|---|---|---|
f968d05ea0 | |||
d6e5dc2167 | |||
460147fea2 | |||
cea44834bb | |||
1af50fd7b8 | |||
b18013025f | |||
399eb60809 | |||
ed67e3506b | |||
d8ff37fc45 | |||
2fcc3bb1ea | |||
2e680c3d1e | |||
af0a0ef41b | |||
bbfccb0bbf | |||
c89eb5d69f | |||
ebde84214e | |||
03fbae7b6d | |||
f90e9596d4 | |||
944f9524e2 | |||
c61050244e | |||
90337adbbc | |||
7b67e41c5b | |||
91db24fcfc | |||
bb53db905f | |||
0e9a1efe46 | |||
289cd3e200 | |||
e0f847e539 | |||
c2842b486e | |||
7235ade42f | |||
850be2df1d | |||
d504501440 | |||
208392f12c | |||
0fe036c640 | |||
a40c41f0b0 | |||
4affa5b710 | |||
4eb574d991 | |||
2c1577ea24 | |||
b87e7e50b6 | |||
36215d50bd | |||
5ff1245d0c | |||
ebd189fb27 | |||
6f724827bd | |||
b6a0982012 | |||
c3e375e8a5 | |||
302409fd83 | |||
a2046461c1 | |||
6660c34120 | |||
b88ccf0ddd | |||
b898bbf94c | |||
787e89eb95 | |||
1022c2c438 | |||
ba21c62ed4 | |||
bfe66c919b | |||
3dacf7f661 | |||
c0a3ae2612 | |||
da612ef789 | |||
df9cb7cf6e | |||
9c1a26110e | |||
0883d18a6c | |||
c7246c61a5 | |||
c5a1431fc0 | |||
f0118a0dff | |||
cffe96e46f | |||
a9256578f0 | |||
05ed202904 | |||
963b63389a | |||
e04706dc74 | |||
04d4ce5ce1 | |||
24cf3730fa | |||
0700be86e2 | |||
7cca509eb3 | |||
7d7193cb63 | |||
1cf10d05ff | |||
2ec25a7729 | |||
2a9065a61e | |||
7518e30dcf | |||
dc3c80e3ce | |||
a25f61f6be | |||
e70fb71a04 | |||
f499630c2b | |||
43319a8588 | |||
d62b943c5d | |||
8baddf2ea3 | |||
600482660b | |||
72ab5c143e | |||
96ab0e7b4c | |||
b60903e2b4 | |||
b4f4d3f267 | |||
6e017c86e8 | |||
afcfc2dca5 | |||
59e22a12a9 | |||
b740ac3e01 | |||
9719f0df03 | |||
d4be599538 | |||
f88195c90a | |||
3b33f7e752 | |||
67a37294f7 | |||
fd88955696 | |||
9d248dbb5a | |||
20ec4104c6 | |||
6c232d116d | |||
2ef78bcd40 | |||
94ce658ab9 | |||
d8cf4cd341 | |||
0360337df9 | |||
119d38ea08 | |||
bee77afb7f | |||
16d4b16872 | |||
951b2346ab | |||
b29ff0e94b |
@ -55,3 +55,7 @@ twitter:
|
||||
|
||||
# インテグレーション用アプリのコンシューマーシークレット
|
||||
consumer_secret:
|
||||
|
||||
# true にすると、リモートのファイルをキャッシュしなくなります(直リンクします)。
|
||||
# ストレージ容量を節約することができますが、「リモートメディアを表示しない」設定をオンにしているユーザーは、リモートの画像などは見えなくなります。
|
||||
preventCache: false
|
||||
|
@ -70,6 +70,7 @@ common:
|
||||
donation: "Spenden"
|
||||
nav: "Navigation"
|
||||
tips: "Tipps"
|
||||
hashtags: "ハッシュタグ"
|
||||
deck:
|
||||
widgets: "Widget hinzufügen:"
|
||||
home: "Startseite"
|
||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
||||
common/views/widgets/posts-monitor.vue:
|
||||
title: "投稿チャート"
|
||||
toggle: "表示を切り替え"
|
||||
common/views/widgets/hashtags.vue:
|
||||
title: "ハッシュタグ"
|
||||
count: "{}人が投稿"
|
||||
common/views/widgets/server.vue:
|
||||
title: "Serverinformationen"
|
||||
toggle: "Sicht umschalten"
|
||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
desktop/views/pages/deck/deck.note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "詳しく..."
|
||||
gotit: "わかった"
|
||||
|
@ -70,6 +70,7 @@ common:
|
||||
donation: "Donation"
|
||||
nav: "Navigation"
|
||||
tips: "Tips"
|
||||
hashtags: "Hashtags"
|
||||
deck:
|
||||
widgets: "Widgets"
|
||||
home: "Home"
|
||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
||||
common/views/widgets/posts-monitor.vue:
|
||||
title: "Chart of posts"
|
||||
toggle: "Toggle views"
|
||||
common/views/widgets/hashtags.vue:
|
||||
title: "Hashtags"
|
||||
count: "{} users mentioned"
|
||||
common/views/widgets/server.vue:
|
||||
title: "Server info"
|
||||
toggle: "Toggle views"
|
||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "Only media posts"
|
||||
is-media-view: "Media view"
|
||||
desktop/views/pages/deck/deck.note.vue:
|
||||
reposted-by: "Reposted by {}"
|
||||
private: "this post is private"
|
||||
deleted: "this post has been deleted"
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "about"
|
||||
gotit: "Got it!"
|
||||
|
@ -70,6 +70,7 @@ common:
|
||||
donation: "寄付のお願い"
|
||||
nav: "ナビゲーション"
|
||||
tips: "ヒント"
|
||||
hashtags: "ハッシュタグ"
|
||||
deck:
|
||||
widgets: "ウィジェット"
|
||||
home: "ホーム"
|
||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
||||
common/views/widgets/posts-monitor.vue:
|
||||
title: "投稿チャート"
|
||||
toggle: "表示を切り替え"
|
||||
common/views/widgets/hashtags.vue:
|
||||
title: "ハッシュタグ"
|
||||
count: "{}人が投稿"
|
||||
common/views/widgets/server.vue:
|
||||
title: "サーバー情報"
|
||||
toggle: "表示を切り替え"
|
||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
desktop/views/pages/deck/deck.note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "詳しく..."
|
||||
gotit: "わかった"
|
||||
|
@ -54,22 +54,23 @@ common:
|
||||
timemachine: "カレンダー(タイムマシン)"
|
||||
activity: "Activité"
|
||||
rss: "Lecteur de flux RSS"
|
||||
memo: "付箋"
|
||||
memo: "Pense-bête"
|
||||
trends: "Tendances"
|
||||
photo-stream: "Flux de photos"
|
||||
posts-monitor: "投稿チャート"
|
||||
posts-monitor: "Graph des publications"
|
||||
slideshow: "Diaporama"
|
||||
version: "Version"
|
||||
broadcast: "Diffusion"
|
||||
notifications: "Notifications"
|
||||
users: "Utilisateurs"
|
||||
polls: "アンケート"
|
||||
polls: "Sondages"
|
||||
post-form: "投稿フォーム"
|
||||
messaging: "Messagerie"
|
||||
server: "Info sur le serveur"
|
||||
donation: "Dons"
|
||||
nav: "Navigation"
|
||||
tips: "Conseils"
|
||||
hashtags: "ハッシュタグ"
|
||||
deck:
|
||||
widgets: "Widgets"
|
||||
home: "Accueil"
|
||||
@ -151,11 +152,11 @@ common/views/components/poll.vue:
|
||||
show-result: "Montrer les résultats"
|
||||
voted: "Voté"
|
||||
common/views/components/poll-editor.vue:
|
||||
no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
|
||||
no-only-one-choice: "Vous devez saisir au moins deux choix."
|
||||
choice-n: "Choix {}"
|
||||
remove: "Supprimer ce choix"
|
||||
add: "+ Ajouter un choix"
|
||||
destroy: "アンケートを破棄"
|
||||
destroy: "Annuler ce sondage"
|
||||
common/views/components/reaction-picker.vue:
|
||||
choose-reaction: "Choisissez votre réaction"
|
||||
common/views/components/signin.vue:
|
||||
@ -222,13 +223,16 @@ common/views/widgets/photo-stream.vue:
|
||||
title: "Flux de photo"
|
||||
no-photos: "Pas de photos"
|
||||
common/views/widgets/posts-monitor.vue:
|
||||
title: "投稿チャート"
|
||||
title: "Graph des publications"
|
||||
toggle: "表示を切り替え"
|
||||
common/views/widgets/hashtags.vue:
|
||||
title: "ハッシュタグ"
|
||||
count: "{}人が投稿"
|
||||
common/views/widgets/server.vue:
|
||||
title: "Info sur le serveur"
|
||||
toggle: "Afficher les vues"
|
||||
common/views/widgets/memo.vue:
|
||||
title: "付箋"
|
||||
title: "Pense-bête"
|
||||
memo: "Écrivez ici !"
|
||||
save: "Enregistrer"
|
||||
desktop/views/components/activity.chart.vue:
|
||||
@ -380,7 +384,7 @@ desktop/views/components/post-form.vue:
|
||||
attach-media-from-drive: "Joindre un media depuis votre Drive"
|
||||
attach-cancel: "Annuler la jointure de fichier"
|
||||
insert-a-kao: "v(‘ω’)v"
|
||||
create-poll: "アンケートを作成"
|
||||
create-poll: "Créer un sondage"
|
||||
text-remain: "{} charactères restants"
|
||||
desktop/views/components/post-form-window.vue:
|
||||
note: "Nouvelle note"
|
||||
@ -414,7 +418,7 @@ desktop/views/components/settings.vue:
|
||||
license: "License"
|
||||
behaviour: "Comportement"
|
||||
fetch-on-scroll: "Chargement lors du défilement"
|
||||
fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。"
|
||||
fetch-on-scroll-desc: "Chargement automatique du contenu lors du défilement de la page."
|
||||
auto-popout: "Fenêtre contextuelle automatique"
|
||||
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
|
||||
advanced: "Paramètres avancés"
|
||||
@ -523,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
|
||||
private: "cette publication est privée"
|
||||
deleted: "cette publication a été supprimée"
|
||||
media-count: "{} médias attachés"
|
||||
poll: "アンケート"
|
||||
poll: "Sondage"
|
||||
desktop/views/components/taskmanager.vue:
|
||||
title: "Gestionnaire de tâches"
|
||||
desktop/views/components/timeline.vue:
|
||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
desktop/views/pages/deck/deck.note.vue:
|
||||
reposted-by: "Reposté par {}"
|
||||
private: "cette publication est privée"
|
||||
deleted: "cette publication a été supprimée"
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "à propos"
|
||||
gotit: "J'ai compris !"
|
||||
@ -639,7 +647,7 @@ desktop/views/widgets/notifications.vue:
|
||||
title: "Notifications"
|
||||
settings: "Réglages"
|
||||
desktop/views/widgets/polls.vue:
|
||||
title: "アンケート"
|
||||
title: "Sondages"
|
||||
refresh: "Afficher d'autres"
|
||||
nothing: "Rien"
|
||||
desktop/views/widgets/post-form.vue:
|
||||
@ -666,7 +674,7 @@ mobile/views/components/drive.vue:
|
||||
nothing-in-drive: "Rien"
|
||||
folder-is-empty: "Ce dossier est vide"
|
||||
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
|
||||
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
|
||||
deletion-alert: "Désolé ! La suppression d’un dossier n’est pas encore implémentée."
|
||||
folder-name: "Nom du dossier"
|
||||
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
|
||||
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
|
||||
|
@ -70,6 +70,7 @@ common:
|
||||
donation: "寄付のお願い"
|
||||
nav: "ナビゲーション"
|
||||
tips: "ヒント"
|
||||
hashtags: "ハッシュタグ"
|
||||
deck:
|
||||
widgets: "ウィジェット"
|
||||
home: "ホーム"
|
||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
||||
common/views/widgets/posts-monitor.vue:
|
||||
title: "投稿チャート"
|
||||
toggle: "表示を切り替え"
|
||||
common/views/widgets/hashtags.vue:
|
||||
title: "ハッシュタグ"
|
||||
count: "{}人が投稿"
|
||||
common/views/widgets/server.vue:
|
||||
title: "サーバー情報"
|
||||
toggle: "表示を切り替え"
|
||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
desktop/views/pages/deck/deck.note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "詳しく..."
|
||||
gotit: "わかった"
|
||||
|
@ -76,6 +76,7 @@ common:
|
||||
donation: "寄付のお願い"
|
||||
nav: "ナビゲーション"
|
||||
tips: "ヒント"
|
||||
hashtags: "ハッシュタグ"
|
||||
|
||||
deck:
|
||||
widgets: "ウィジェット"
|
||||
@ -254,6 +255,11 @@ common/views/widgets/posts-monitor.vue:
|
||||
title: "投稿チャート"
|
||||
toggle: "表示を切り替え"
|
||||
|
||||
common/views/widgets/hashtags.vue:
|
||||
title: "ハッシュタグ"
|
||||
count: "{}人が投稿"
|
||||
empty: "トレンドなし"
|
||||
|
||||
common/views/widgets/server.vue:
|
||||
title: "サーバー情報"
|
||||
toggle: "表示を切り替え"
|
||||
@ -672,6 +678,11 @@ desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
|
||||
desktop/views/pages/deck/deck.note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "詳しく..."
|
||||
gotit: "わかった"
|
||||
|
@ -70,6 +70,7 @@ common:
|
||||
donation: "寄付のお願い"
|
||||
nav: "ナビゲーション"
|
||||
tips: "ヒント"
|
||||
hashtags: "ハッシュタグ"
|
||||
deck:
|
||||
widgets: "ウィジェット"
|
||||
home: "ホーム"
|
||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
||||
common/views/widgets/posts-monitor.vue:
|
||||
title: "投稿チャート"
|
||||
toggle: "表示を切り替え"
|
||||
common/views/widgets/hashtags.vue:
|
||||
title: "ハッシュタグ"
|
||||
count: "{}人が投稿"
|
||||
common/views/widgets/server.vue:
|
||||
title: "サーバー情報"
|
||||
toggle: "表示を切り替え"
|
||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
desktop/views/pages/deck/deck.note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "詳しく..."
|
||||
gotit: "わかった"
|
||||
|
@ -54,7 +54,7 @@ common:
|
||||
timemachine: "Kalendarz (wehikuł czasu)"
|
||||
activity: "Aktywność"
|
||||
rss: "Czytnik RSS"
|
||||
memo: "付箋"
|
||||
memo: "Notatka"
|
||||
trends: "Na czasie"
|
||||
photo-stream: "Photostream"
|
||||
posts-monitor: "Wykres wpisów"
|
||||
@ -63,13 +63,14 @@ common:
|
||||
broadcast: "Transmisja"
|
||||
notifications: "Powiadomienia"
|
||||
users: "Polecani użytkownicy"
|
||||
polls: "アンケート"
|
||||
polls: "Ankiety"
|
||||
post-form: "Formularz tworzenia"
|
||||
messaging: "Wiadomości"
|
||||
server: "Informacje o serwerze"
|
||||
donation: "Dotacje"
|
||||
nav: "Nawigacja"
|
||||
tips: "Wskazówki"
|
||||
hashtags: "Hashtagi"
|
||||
deck:
|
||||
widgets: "Widżety"
|
||||
home: "Strona główna"
|
||||
@ -151,11 +152,11 @@ common/views/components/poll.vue:
|
||||
show-result: "Pokaż wyniki"
|
||||
voted: "Zagłosowano"
|
||||
common/views/components/poll-editor.vue:
|
||||
no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
|
||||
no-only-one-choice: "Musisz wprowadzić przynajmniej dwie opcje."
|
||||
choice-n: "Opcja {}"
|
||||
remove: "Usuń tą opcję"
|
||||
add: "+ Dodaj opcję"
|
||||
destroy: "アンケートを破棄"
|
||||
destroy: "Usuń tę ankietę"
|
||||
common/views/components/reaction-picker.vue:
|
||||
choose-reaction: "Wybierz reakcję"
|
||||
common/views/components/signin.vue:
|
||||
@ -224,11 +225,14 @@ common/views/widgets/photo-stream.vue:
|
||||
common/views/widgets/posts-monitor.vue:
|
||||
title: "Wykres wpisów"
|
||||
toggle: "Przełącz widok"
|
||||
common/views/widgets/hashtags.vue:
|
||||
title: "Hashtagi"
|
||||
count: "Wspomniany przez {} użytkowników"
|
||||
common/views/widgets/server.vue:
|
||||
title: "Informacje o serwerze"
|
||||
toggle: "Przełącz widok"
|
||||
common/views/widgets/memo.vue:
|
||||
title: "付箋"
|
||||
title: "Notatka"
|
||||
memo: "Napisz tutaj!"
|
||||
save: "Zapisz"
|
||||
desktop/views/components/activity.chart.vue:
|
||||
@ -380,7 +384,7 @@ desktop/views/components/post-form.vue:
|
||||
attach-media-from-drive: "Załącz zawartość multimedialną z dysku"
|
||||
attach-cancel: "Usuń załącznik"
|
||||
insert-a-kao: "v(‘ω’)v"
|
||||
create-poll: "アンケートを作成"
|
||||
create-poll: "Utwórz ankietę"
|
||||
text-remain: "pozostałe znaki: {}"
|
||||
desktop/views/components/post-form-window.vue:
|
||||
note: "Nowy wpis"
|
||||
@ -523,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
|
||||
private: "ten wpis jest prywatny"
|
||||
deleted: "ten wpis został usunięty"
|
||||
media-count: "{}zawartości multimedialnej"
|
||||
poll: "アンケート"
|
||||
poll: "Ankieta"
|
||||
desktop/views/components/taskmanager.vue:
|
||||
title: "Menedżer zadań"
|
||||
desktop/views/components/timeline.vue:
|
||||
@ -574,8 +578,12 @@ desktop/views/components/window.vue:
|
||||
popout: "Pop-out"
|
||||
close: "Zamknij"
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
is-media-only: "Tylko wpisy z zawartością multimedialną"
|
||||
is-media-view: "Widok multimediów"
|
||||
desktop/views/pages/deck/deck.note.vue:
|
||||
reposted-by: "Udostępniono przez {}"
|
||||
private: "ten wpis jest prywatny"
|
||||
deleted: "ten wpis został usunięty"
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "O Misskey"
|
||||
gotit: "Rozumiem!"
|
||||
@ -639,7 +647,7 @@ desktop/views/widgets/notifications.vue:
|
||||
title: "Powiadomienia"
|
||||
settings: "Ustawienia"
|
||||
desktop/views/widgets/polls.vue:
|
||||
title: "アンケート"
|
||||
title: "Ankiety"
|
||||
refresh: "Pokaż inne"
|
||||
nothing: "Pusto"
|
||||
desktop/views/widgets/post-form.vue:
|
||||
@ -738,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
|
||||
private: "ten wpis jest prywatny"
|
||||
deleted: "ten wpis został usunięty"
|
||||
media-count: "{}zawartości multimedialnej"
|
||||
poll: "アンケート"
|
||||
poll: "Ankieta"
|
||||
mobile/views/components/timeline.vue:
|
||||
empty: "Brak wpisów"
|
||||
load-more: "Więcej"
|
||||
|
@ -70,6 +70,7 @@ common:
|
||||
donation: "寄付のお願い"
|
||||
nav: "ナビゲーション"
|
||||
tips: "ヒント"
|
||||
hashtags: "ハッシュタグ"
|
||||
deck:
|
||||
widgets: "ウィジェット"
|
||||
home: "ホーム"
|
||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
||||
common/views/widgets/posts-monitor.vue:
|
||||
title: "投稿チャート"
|
||||
toggle: "表示を切り替え"
|
||||
common/views/widgets/hashtags.vue:
|
||||
title: "ハッシュタグ"
|
||||
count: "{}人が投稿"
|
||||
common/views/widgets/server.vue:
|
||||
title: "サーバー情報"
|
||||
toggle: "表示を切り替え"
|
||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
desktop/views/pages/deck/deck.note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "詳しく..."
|
||||
gotit: "わかった"
|
||||
|
@ -70,6 +70,7 @@ common:
|
||||
donation: "寄付のお願い"
|
||||
nav: "ナビゲーション"
|
||||
tips: "ヒント"
|
||||
hashtags: "ハッシュタグ"
|
||||
deck:
|
||||
widgets: "ウィジェット"
|
||||
home: "ホーム"
|
||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
||||
common/views/widgets/posts-monitor.vue:
|
||||
title: "投稿チャート"
|
||||
toggle: "表示を切り替え"
|
||||
common/views/widgets/hashtags.vue:
|
||||
title: "ハッシュタグ"
|
||||
count: "{}人が投稿"
|
||||
common/views/widgets/server.vue:
|
||||
title: "サーバー情報"
|
||||
toggle: "表示を切り替え"
|
||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
desktop/views/pages/deck/deck.note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "詳しく..."
|
||||
gotit: "わかった"
|
||||
|
@ -70,6 +70,7 @@ common:
|
||||
donation: "寄付のお願い"
|
||||
nav: "ナビゲーション"
|
||||
tips: "ヒント"
|
||||
hashtags: "ハッシュタグ"
|
||||
deck:
|
||||
widgets: "ウィジェット"
|
||||
home: "ホーム"
|
||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
||||
common/views/widgets/posts-monitor.vue:
|
||||
title: "投稿チャート"
|
||||
toggle: "表示を切り替え"
|
||||
common/views/widgets/hashtags.vue:
|
||||
title: "ハッシュタグ"
|
||||
count: "{}人が投稿"
|
||||
common/views/widgets/server.vue:
|
||||
title: "サーバー情報"
|
||||
toggle: "表示を切り替え"
|
||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
desktop/views/pages/deck/deck.note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "詳しく..."
|
||||
gotit: "わかった"
|
||||
|
124
package.json
124
package.json
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "2.35.2",
|
||||
"clientVersion": "1.0.6360",
|
||||
"version": "2.37.7",
|
||||
"clientVersion": "1.0.6474",
|
||||
"codename": "nighthike",
|
||||
"main": "./built/index.js",
|
||||
"private": true,
|
||||
@ -29,6 +29,66 @@
|
||||
"@fortawesome/fontawesome-free-solid": "5.0.2",
|
||||
"@koa/cors": "2.2.1",
|
||||
"@prezzemolo/rap": "0.1.2",
|
||||
"autwh": "0.1.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"cafy": "8.0.0",
|
||||
"chalk": "2.4.1",
|
||||
"crc-32": "1.2.0",
|
||||
"debug": "3.1.0",
|
||||
"deepcopy": "0.6.3",
|
||||
"diskusage": "0.2.4",
|
||||
"elasticsearch": "15.0.0",
|
||||
"emojilib": "2.2.12",
|
||||
"escape-regexp": "0.0.1",
|
||||
"file-type": "8.0.0",
|
||||
"gm": "1.23.1",
|
||||
"http-signature": "1.2.0",
|
||||
"is-root": "2.0.0",
|
||||
"is-url": "1.2.4",
|
||||
"js-yaml": "3.11.0",
|
||||
"jsdom": "11.11.0",
|
||||
"koa": "2.5.1",
|
||||
"koa-bodyparser": "4.2.1",
|
||||
"koa-compress": "3.0.0",
|
||||
"koa-favicon": "2.0.1",
|
||||
"koa-json-body": "5.3.0",
|
||||
"koa-logger": "3.2.0",
|
||||
"koa-mount": "3.0.0",
|
||||
"koa-multer": "1.0.2",
|
||||
"koa-router": "7.4.0",
|
||||
"koa-send": "4.1.3",
|
||||
"koa-slow": "2.1.0",
|
||||
"koa-views": "6.1.4",
|
||||
"kue": "0.11.6",
|
||||
"mongodb": "3.0.10",
|
||||
"monk": "6.0.6",
|
||||
"ms": "2.1.1",
|
||||
"nopt": "4.0.1",
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "5.0.0",
|
||||
"prominence": "0.2.0",
|
||||
"promise-sequential": "1.1.1",
|
||||
"punycode": "2.1.1",
|
||||
"qrcode": "1.2.0",
|
||||
"ratelimiter": "3.0.3",
|
||||
"recaptcha-promise": "0.1.3",
|
||||
"reconnecting-websocket": "3.2.2",
|
||||
"redis": "2.8.0",
|
||||
"request": "2.87.0",
|
||||
"request-promise-native": "1.0.5",
|
||||
"rndstr": "1.0.0",
|
||||
"speakeasy": "2.0.0",
|
||||
"summaly": "2.0.6",
|
||||
"tcp-port-used": "0.1.2",
|
||||
"tmp": "0.0.33",
|
||||
"uuid": "3.2.1",
|
||||
"web-push": "3.3.1",
|
||||
"webfinger.js": "2.6.6",
|
||||
"websocket": "1.0.26",
|
||||
"ws": "5.2.0",
|
||||
"xev": "2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prezzemolo/zip": "0.0.3",
|
||||
"@types/bcryptjs": "2.4.1",
|
||||
"@types/debug": "0.0.30",
|
||||
@ -84,30 +144,17 @@
|
||||
"@types/ws": "5.1.1",
|
||||
"animejs": "2.2.0",
|
||||
"autosize": "4.0.2",
|
||||
"autwh": "0.1.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bootstrap-vue": "2.0.0-rc.6",
|
||||
"cafy": "8.0.0",
|
||||
"chalk": "2.4.1",
|
||||
"crc-32": "1.2.0",
|
||||
"css-loader": "0.28.11",
|
||||
"debug": "3.1.0",
|
||||
"deep-equal": "1.0.1",
|
||||
"deepcopy": "0.6.3",
|
||||
"diskusage": "0.2.4",
|
||||
"dompurify": "1.0.4",
|
||||
"elasticsearch": "15.0.0",
|
||||
"element-ui": "2.3.9",
|
||||
"emojilib": "2.2.12",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eslint": "4.19.1",
|
||||
"eslint-plugin-vue": "4.5.0",
|
||||
"eventemitter3": "3.1.0",
|
||||
"exif-js": "2.3.0",
|
||||
"file-loader": "1.1.11",
|
||||
"file-type": "8.0.0",
|
||||
"fuckadblock": "3.2.1",
|
||||
"gm": "1.23.1",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
"gulp-htmlmin": "4.0.0",
|
||||
@ -125,71 +172,32 @@
|
||||
"hard-source-webpack-plugin": "0.6.10",
|
||||
"highlight.js": "9.12.0",
|
||||
"html-minifier": "3.5.16",
|
||||
"http-signature": "1.2.0",
|
||||
"inquirer": "5.2.0",
|
||||
"is-root": "2.0.0",
|
||||
"is-url": "1.2.4",
|
||||
"js-yaml": "3.11.0",
|
||||
"jsdom": "11.11.0",
|
||||
"koa": "2.5.1",
|
||||
"koa-bodyparser": "4.2.1",
|
||||
"koa-compress": "3.0.0",
|
||||
"koa-favicon": "2.0.1",
|
||||
"koa-json-body": "5.3.0",
|
||||
"koa-logger": "3.2.0",
|
||||
"koa-mount": "3.0.0",
|
||||
"koa-multer": "1.0.2",
|
||||
"koa-router": "7.4.0",
|
||||
"koa-send": "4.1.3",
|
||||
"koa-slow": "2.1.0",
|
||||
"koa-views": "6.1.4",
|
||||
"kue": "0.11.6",
|
||||
"license-checker": "20.0.0",
|
||||
"loader-utils": "1.1.0",
|
||||
"mecab-async": "0.1.2",
|
||||
"mkdirp": "0.5.1",
|
||||
"mocha": "5.2.0",
|
||||
"moji": "0.5.1",
|
||||
"mongodb": "3.0.10",
|
||||
"monk": "6.0.6",
|
||||
"ms": "2.1.1",
|
||||
"nan": "2.10.0",
|
||||
"node-sass": "4.9.0",
|
||||
"node-sass-json-importer": "3.2.0",
|
||||
"nopt": "4.0.1",
|
||||
"nprogress": "0.2.0",
|
||||
"object-assign-deep": "0.4.0",
|
||||
"on-build-webpack": "0.1.0",
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "5.0.0",
|
||||
"progress-bar-webpack-plugin": "1.11.0",
|
||||
"prominence": "0.2.0",
|
||||
"promise-sequential": "1.1.1",
|
||||
"pug": "2.0.3",
|
||||
"punycode": "2.1.1",
|
||||
"qrcode": "1.2.0",
|
||||
"ratelimiter": "3.0.3",
|
||||
"recaptcha-promise": "0.1.3",
|
||||
"reconnecting-websocket": "3.2.2",
|
||||
"redis": "2.8.0",
|
||||
"request": "2.87.0",
|
||||
"request-promise-native": "1.0.5",
|
||||
"rimraf": "2.6.2",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"sass-loader": "7.0.1",
|
||||
"seedrandom": "2.4.3",
|
||||
"single-line-log": "1.1.2",
|
||||
"speakeasy": "2.0.0",
|
||||
"style-loader": "0.21.0",
|
||||
"stylus": "0.54.5",
|
||||
"stylus-loader": "3.0.2",
|
||||
"summaly": "2.0.6",
|
||||
"swagger-jsdoc": "1.9.7",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"tcp-port-used": "0.1.2",
|
||||
"textarea-caret": "3.1.0",
|
||||
"tmp": "0.0.33",
|
||||
"ts-loader": "4.3.0",
|
||||
"ts-node": "6.0.4",
|
||||
"tslint": "5.10.0",
|
||||
@ -197,7 +205,6 @@
|
||||
"typescript-eslint-parser": "15.0.0",
|
||||
"uglify-es": "3.3.9",
|
||||
"url-loader": "1.0.1",
|
||||
"uuid": "3.2.1",
|
||||
"v-animate-css": "0.0.2",
|
||||
"vue": "2.5.16",
|
||||
"vue-cropperjs": "2.2.0",
|
||||
@ -210,12 +217,7 @@
|
||||
"vuedraggable": "2.16.0",
|
||||
"vuex": "3.0.1",
|
||||
"vuex-persistedstate": "^2.5.4",
|
||||
"web-push": "3.3.1",
|
||||
"webfinger.js": "2.6.6",
|
||||
"webpack": "4.9.1",
|
||||
"webpack-cli": "2.1.4",
|
||||
"websocket": "1.0.26",
|
||||
"ws": "5.2.0",
|
||||
"xev": "2.0.1"
|
||||
"webpack-cli": "2.1.4"
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,17 @@ export default Vue.component('mk-note-html', {
|
||||
ast = this.ast;
|
||||
}
|
||||
|
||||
if (ast.filter(x => x.type != 'hashtag').length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (
|
||||
ast[ast.length - 1].type == 'hashtag' ||
|
||||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') ||
|
||||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n')) {
|
||||
ast.pop();
|
||||
}
|
||||
|
||||
// Parse ast to DOM
|
||||
const els = flatten(ast.map(token => {
|
||||
switch (token.type) {
|
||||
@ -92,7 +103,7 @@ export default Vue.component('mk-note-html', {
|
||||
case 'hashtag':
|
||||
return createElement('a', {
|
||||
attrs: {
|
||||
href: `${url}/search?q=${token.content}`,
|
||||
href: `${url}/tags/${token.content}`,
|
||||
target: '_blank'
|
||||
}
|
||||
}, token.content);
|
||||
|
@ -58,18 +58,21 @@ export default Vue.extend({
|
||||
},
|
||||
created() {
|
||||
if (this.mode == 'relative' || this.mode == 'detail') {
|
||||
this.tick();
|
||||
this.tickId = setInterval(this.tick, 10000);
|
||||
this.tickId = window.requestAnimationFrame(this.tick);
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
if (this.mode === 'relative' || this.mode === 'detail') {
|
||||
clearInterval(this.tickId);
|
||||
window.clearTimeout(this.tickId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tick() {
|
||||
this.now = new Date();
|
||||
|
||||
this.tickId = setTimeout(() => {
|
||||
window.requestAnimationFrame(this.tick);
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -203,6 +203,7 @@ root(isDark)
|
||||
justify-content center
|
||||
align-items center
|
||||
margin-right 10px
|
||||
width 16px
|
||||
|
||||
> *:last-child
|
||||
flex 1 1 auto
|
||||
|
89
src/client/app/common/views/widgets/hashtags.chart.vue
Normal file
89
src/client/app/common/views/widgets/hashtags.chart.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" style="overflow:visible">
|
||||
<defs>
|
||||
<linearGradient :id="gradientId" x1="0" x2="0" y1="1" y2="0">
|
||||
<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
|
||||
<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
|
||||
</linearGradient>
|
||||
<mask :id="maskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
|
||||
<polygon
|
||||
:points="polygonPoints"
|
||||
fill="#fff"
|
||||
fill-opacity="0.5"/>
|
||||
<polyline
|
||||
:points="polylinePoints"
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
stroke-width="2"/>
|
||||
<circle
|
||||
:cx="headX"
|
||||
:cy="headY"
|
||||
r="3"
|
||||
fill="#fff"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<rect
|
||||
x="-10" y="-10"
|
||||
:width="viewBoxX + 20" :height="viewBoxY + 20"
|
||||
:style="`stroke: none; fill: url(#${ gradientId }); mask: url(#${ maskId })`"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
src: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
viewBoxX: 50,
|
||||
viewBoxY: 30,
|
||||
gradientId: uuid(),
|
||||
maskId: uuid(),
|
||||
polylinePoints: '',
|
||||
polygonPoints: '',
|
||||
headX: null,
|
||||
headY: null,
|
||||
clock: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
src() {
|
||||
this.draw();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.draw();
|
||||
|
||||
// Vueが何故かWatchを発動させない場合があるので
|
||||
this.clock = setInterval(this.draw, 1000);
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.clock);
|
||||
},
|
||||
methods: {
|
||||
draw() {
|
||||
const stats = this.src.slice().reverse();
|
||||
const peak = Math.max.apply(null, stats) || 1;
|
||||
|
||||
const polylinePoints = stats.map((n, i) => [
|
||||
i * (this.viewBoxX / (stats.length - 1)),
|
||||
(1 - (n / peak)) * this.viewBoxY
|
||||
]);
|
||||
|
||||
this.polylinePoints = polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
||||
|
||||
this.polygonPoints = `0,${ this.viewBoxY } ${ this.polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
|
||||
|
||||
this.headX = polylinePoints[polylinePoints.length - 1][0];
|
||||
this.headY = polylinePoints[polylinePoints.length - 1][1];
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
123
src/client/app/common/views/widgets/hashtags.vue
Normal file
123
src/client/app/common/views/widgets/hashtags.vue
Normal file
@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="mkw-hashtags">
|
||||
<mk-widget-container :show-header="!props.compact">
|
||||
<template slot="header">%fa:hashtag%%i18n:@title%</template>
|
||||
|
||||
<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'">
|
||||
<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 v-for="stat in stats" :key="stat.tag">
|
||||
<div class="tag">
|
||||
<router-link :to="`/tags/${ stat.tag }`" :title="stat.tag">#{{ stat.tag }}</router-link>
|
||||
<p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p>
|
||||
</div>
|
||||
<x-chart class="chart" :src="stat.chart"/>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</mk-widget-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from '../../../common/define-widget';
|
||||
import XChart from './hashtags.chart.vue';
|
||||
|
||||
export default define({
|
||||
name: 'hashtags',
|
||||
props: () => ({
|
||||
compact: false
|
||||
})
|
||||
}).extend({
|
||||
components: {
|
||||
XChart
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
stats: [],
|
||||
fetching: true,
|
||||
clock: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetch();
|
||||
this.clock = setInterval(this.fetch, 1000 * 60);
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.clock);
|
||||
},
|
||||
methods: {
|
||||
func() {
|
||||
this.props.compact = !this.props.compact;
|
||||
this.save();
|
||||
},
|
||||
fetch() {
|
||||
(this as any).api('hashtags/trend').then(stats => {
|
||||
this.stats = stats;
|
||||
this.fetching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
root(isDark)
|
||||
.mkw-hashtags--body
|
||||
> .fetching
|
||||
> .empty
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> div
|
||||
.chart-enter
|
||||
.chart-leave-to
|
||||
opacity 0
|
||||
transform translateY(-30px)
|
||||
|
||||
> *
|
||||
transition transform .3s ease, opacity .3s ease
|
||||
|
||||
> div
|
||||
display flex
|
||||
align-items center
|
||||
padding 14px 16px
|
||||
|
||||
&:not(:last-child)
|
||||
border-bottom solid 1px isDark ? #393f4f : #eee
|
||||
|
||||
> .tag
|
||||
flex 1
|
||||
overflow hidden
|
||||
font-size 14px
|
||||
color isDark ? #9baec8 : #65727b
|
||||
|
||||
> a
|
||||
display block
|
||||
width 100%
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
color inherit
|
||||
|
||||
> p
|
||||
margin 0
|
||||
font-size 75%
|
||||
opacity 0.7
|
||||
|
||||
> .chart
|
||||
height 30px
|
||||
|
||||
.mkw-hashtags[data-darkmode]
|
||||
root(true)
|
||||
|
||||
.mkw-hashtags:not([data-darkmode])
|
||||
root(false)
|
||||
|
||||
</style>
|
@ -13,6 +13,7 @@ import wSlideshow from './slideshow.vue';
|
||||
import wTips from './tips.vue';
|
||||
import wDonation from './donation.vue';
|
||||
import wNav from './nav.vue';
|
||||
import wHashtags from './hashtags.vue';
|
||||
|
||||
Vue.component('mkw-analog-clock', wAnalogClock);
|
||||
Vue.component('mkw-nav', wNav);
|
||||
@ -27,3 +28,4 @@ Vue.component('mkw-posts-monitor', wPostsMonitor);
|
||||
Vue.component('mkw-memo', wMemo);
|
||||
Vue.component('mkw-rss', wRss);
|
||||
Vue.component('mkw-version', wVersion);
|
||||
Vue.component('mkw-hashtags', wHashtags);
|
||||
|
@ -33,6 +33,7 @@ import MkHomeCustomize from './views/pages/home-customize.vue';
|
||||
import MkMessagingRoom from './views/pages/messaging-room.vue';
|
||||
import MkNote from './views/pages/note.vue';
|
||||
import MkSearch from './views/pages/search.vue';
|
||||
import MkTag from './views/pages/tag.vue';
|
||||
import MkOthello from './views/pages/othello.vue';
|
||||
|
||||
/**
|
||||
@ -60,6 +61,7 @@ init(async (launch) => {
|
||||
{ path: '/i/lists/:list', component: MkUserList },
|
||||
{ path: '/selectdrive', component: MkSelectDrive },
|
||||
{ path: '/search', component: MkSearch },
|
||||
{ path: '/tags/:tag', component: MkTag },
|
||||
{ path: '/othello', component: MkOthello },
|
||||
{ path: '/othello/:game', component: MkOthello },
|
||||
{ path: '/@:user', component: MkUser },
|
||||
|
@ -23,6 +23,7 @@
|
||||
<option value="post-form">%i18n:common.widgets.post-form%</option>
|
||||
<option value="messaging">%i18n:common.widgets.messaging%</option>
|
||||
<option value="memo">%i18n:common.widgets.memo%</option>
|
||||
<option value="hashtags">%i18n:common.widgets.hashtags%</option>
|
||||
<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
|
||||
<option value="server">%i18n:common.widgets.server%</option>
|
||||
<option value="donation">%i18n:common.widgets.donation%</option>
|
||||
|
@ -48,7 +48,7 @@
|
||||
<mk-poll v-if="p.poll" :note="p"/>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
|
||||
</div>
|
||||
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
||||
<div class="map" v-if="p.geo" ref="map"></div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="mk-note-preview" :title="title">
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
<mk-avatar class="avatar" :user="note.user" v-if="!mini"/>
|
||||
<div class="main">
|
||||
<mk-note-header class="header" :note="note" :mini="true"/>
|
||||
<div class="body">
|
||||
@ -15,7 +15,17 @@ import Vue from 'vue';
|
||||
import dateStringify from '../../../common/scripts/date-stringify';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['note'],
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
mini: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title(): string {
|
||||
return dateStringify(this.note.createdAt);
|
||||
|
@ -33,7 +33,7 @@
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
|
||||
</div>
|
||||
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
|
||||
<div class="map" v-if="p.geo" ref="map"></div>
|
||||
|
@ -17,7 +17,11 @@ export default Vue.extend({
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
location.href = `/search?q=${encodeURIComponent(this.q)}`;
|
||||
if (this.q.startsWith('#')) {
|
||||
this.$router.push(`/tags/${encodeURIComponent(this.q.substr(1))}`);
|
||||
} else {
|
||||
this.$router.push(`/search?q=${encodeURIComponent(this.q)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -33,11 +33,11 @@
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
|
||||
</div>
|
||||
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
||||
<div class="renote" v-if="p.renote">
|
||||
<mk-note-preview :note="p.renote"/>
|
||||
<mk-note-preview :note="p.renote" :mini="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq">
|
||||
<template v-if="edit">
|
||||
<header>
|
||||
<select v-model="widgetAdderSelected">
|
||||
<select v-model="widgetAdderSelected" @change="addWidget">
|
||||
<option value="profile">%i18n:common.widgets.profile%</option>
|
||||
<option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
|
||||
<option value="calendar">%i18n:common.widgets.calendar%</option>
|
||||
@ -23,26 +23,22 @@
|
||||
<option value="post-form">%i18n:common.widgets.post-form%</option>
|
||||
<option value="messaging">%i18n:common.widgets.messaging%</option>
|
||||
<option value="memo">%i18n:common.widgets.memo%</option>
|
||||
<option value="hashtags">%i18n:common.widgets.hashtags%</option>
|
||||
<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
|
||||
<option value="server">%i18n:common.widgets.server%</option>
|
||||
<option value="donation">%i18n:common.widgets.donation%</option>
|
||||
<option value="nav">%i18n:common.widgets.nav%</option>
|
||||
<option value="tips">%i18n:common.widgets.tips%</option>
|
||||
</select>
|
||||
<button @click="addWidget">%i18n:@add%</button>
|
||||
</header>
|
||||
<x-draggable
|
||||
:list="column.widgets"
|
||||
:options="{ handle: '.handle', animation: 150 }"
|
||||
:options="{ animation: 150 }"
|
||||
@sort="onWidgetSort"
|
||||
>
|
||||
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id">
|
||||
<header>
|
||||
<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
|
||||
</header>
|
||||
<div @click="widgetFunc(widget.id)">
|
||||
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
|
||||
</div>
|
||||
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="widgetFunc(widget.id)">
|
||||
<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
|
||||
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
|
||||
</div>
|
||||
</x-draggable>
|
||||
</template>
|
||||
@ -141,6 +137,13 @@ export default Vue.extend({
|
||||
|
||||
root(isDark)
|
||||
.gqpwvtwtprsbmnssnbicggtwqhmylhnq
|
||||
> header
|
||||
padding 16px
|
||||
|
||||
> *
|
||||
width 100%
|
||||
padding 4px
|
||||
|
||||
.widget, .customize-container
|
||||
margin 8px
|
||||
|
||||
@ -148,7 +151,21 @@ root(isDark)
|
||||
margin-top 0
|
||||
|
||||
.customize-container
|
||||
background #fff
|
||||
cursor move
|
||||
|
||||
> *:not(.remove)
|
||||
pointer-events none
|
||||
|
||||
> .remove
|
||||
position absolute
|
||||
z-index 1
|
||||
top 8px
|
||||
right 8px
|
||||
width 32px
|
||||
height 32px
|
||||
color #fff
|
||||
background rgba(#000, 0.7)
|
||||
border-radius 4px
|
||||
|
||||
> header
|
||||
color isDark ? #fff : #000
|
||||
|
128
src/client/app/desktop/views/pages/tag.vue
Normal file
128
src/client/app/desktop/views/pages/tag.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<mk-ui>
|
||||
<header :class="$style.header">
|
||||
<h1>#{{ $route.params.tag }}</h1>
|
||||
</header>
|
||||
<div :class="$style.loading" v-if="fetching">
|
||||
<mk-ellipsis-icon/>
|
||||
</div>
|
||||
<p :class="$style.empty" v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p>
|
||||
<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
|
||||
const limit = 20;
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
existMore: false,
|
||||
offset: 0,
|
||||
empty: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
$route: 'fetch'
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.onDocumentKeydown);
|
||||
window.addEventListener('scroll', this.onScroll, { passive: true });
|
||||
|
||||
this.fetch();
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.onDocumentKeydown);
|
||||
window.removeEventListener('scroll', this.onScroll);
|
||||
},
|
||||
methods: {
|
||||
onDocumentKeydown(e) {
|
||||
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
|
||||
if (e.which == 84) { // t
|
||||
(this.$refs.timeline as any).focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.fetching = true;
|
||||
Progress.start();
|
||||
|
||||
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
|
||||
(this as any).api('notes/search_by_tag', {
|
||||
limit: limit + 1,
|
||||
offset: this.offset,
|
||||
tag: this.$route.params.tag
|
||||
}).then(notes => {
|
||||
if (notes.length == 0) this.empty = true;
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
this.existMore = true;
|
||||
}
|
||||
res(notes);
|
||||
this.fetching = false;
|
||||
Progress.done();
|
||||
}, rej);
|
||||
}));
|
||||
},
|
||||
more() {
|
||||
this.offset += limit;
|
||||
|
||||
const promise = (this as any).api('notes/search_by_tag', {
|
||||
limit: limit + 1,
|
||||
offset: this.offset,
|
||||
tag: this.$route.params.tag
|
||||
});
|
||||
|
||||
promise.then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
} else {
|
||||
this.existMore = false;
|
||||
}
|
||||
notes.forEach(n => (this.$refs.timeline as any).append(n));
|
||||
this.moreFetching = false;
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" module>
|
||||
.header
|
||||
width 100%
|
||||
max-width 600px
|
||||
margin 0 auto
|
||||
color #555
|
||||
|
||||
.notes
|
||||
width 600px
|
||||
margin 0 auto
|
||||
border solid 1px rgba(#000, 0.075)
|
||||
border-radius 6px
|
||||
overflow hidden
|
||||
|
||||
.loading
|
||||
padding 64px 0
|
||||
|
||||
.empty
|
||||
display block
|
||||
margin 0 auto
|
||||
padding 32px
|
||||
max-width 400px
|
||||
text-align center
|
||||
color #999
|
||||
|
||||
> [data-fa]
|
||||
display block
|
||||
margin-bottom 16px
|
||||
font-size 3em
|
||||
color #ccc
|
||||
|
||||
</style>
|
@ -41,7 +41,7 @@
|
||||
<mk-note-html v-if="p.text" :text="p.text" :i="$store.state.i"/>
|
||||
</div>
|
||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
|
||||
</div>
|
||||
<div class="media" v-if="p.media.length > 0">
|
||||
<mk-media-list :media-list="p.media" :raw="true"/>
|
||||
|
@ -33,7 +33,7 @@
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
|
||||
</div>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
||||
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
||||
|
@ -15,6 +15,7 @@
|
||||
<option value="rss">%i18n:common.widgets.rss%</option>
|
||||
<option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
|
||||
<option value="slideshow">%i18n:common.widgets.slideshow%</option>
|
||||
<option value="hashtags">%i18n:common.widgets.hashtags%</option>
|
||||
<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
|
||||
<option value="version">%i18n:common.widgets.version%</option>
|
||||
<option value="server">%i18n:common.widgets.server%</option>
|
||||
|
60
src/daemons/hashtags-stats-child.ts
Normal file
60
src/daemons/hashtags-stats-child.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import Note from '../models/note';
|
||||
|
||||
// 10分
|
||||
const interval = 1000 * 60 * 10;
|
||||
|
||||
async function tick() {
|
||||
const res = await Note.aggregate([{
|
||||
$match: {
|
||||
createdAt: {
|
||||
$gt: new Date(Date.now() - interval)
|
||||
},
|
||||
tags: {
|
||||
$exists: true,
|
||||
$ne: []
|
||||
}
|
||||
}
|
||||
}, {
|
||||
$unwind: '$tags'
|
||||
}, {
|
||||
$group: {
|
||||
_id: '$tags',
|
||||
count: {
|
||||
$sum: 1
|
||||
}
|
||||
}
|
||||
}, {
|
||||
$group: {
|
||||
_id: null,
|
||||
tags: {
|
||||
$push: {
|
||||
tag: '$_id',
|
||||
count: '$count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
$project: {
|
||||
_id: false,
|
||||
tags: true
|
||||
}
|
||||
}]) as {
|
||||
tags: Array<{
|
||||
tag: string;
|
||||
count: number;
|
||||
}>
|
||||
};
|
||||
|
||||
const stats = res.tags
|
||||
.sort((a, b) => a.count - b.count)
|
||||
.map(tag => [tag.tag, tag.count])
|
||||
.slice(0, 10);
|
||||
|
||||
console.log(stats);
|
||||
|
||||
process.send(stats);
|
||||
}
|
||||
|
||||
tick();
|
||||
|
||||
setInterval(tick, interval);
|
20
src/daemons/hashtags-stats.ts
Normal file
20
src/daemons/hashtags-stats.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import * as childProcess from 'child_process';
|
||||
import Xev from 'xev';
|
||||
|
||||
const ev = new Xev();
|
||||
|
||||
export default function() {
|
||||
const log = [];
|
||||
|
||||
const p = childProcess.fork(__dirname + '/hashtags-stats-child.js');
|
||||
|
||||
p.on('message', stats => {
|
||||
ev.emit('hashtagsStats', stats);
|
||||
log.push(stats);
|
||||
if (log.length > 30) log.shift();
|
||||
});
|
||||
|
||||
ev.on('requestHashTagsStatsLog', id => {
|
||||
ev.emit('hashtagsStatsLog:' + id, log);
|
||||
});
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import Note from './models/note';
|
||||
import Note from '../models/note';
|
||||
|
||||
const interval = 5000;
|
||||
|
||||
setInterval(async () => {
|
||||
async function tick() {
|
||||
const [all, local] = await Promise.all([Note.count({
|
||||
createdAt: {
|
||||
$gte: new Date(Date.now() - interval)
|
||||
@ -19,4 +19,8 @@ setInterval(async () => {
|
||||
};
|
||||
|
||||
process.send(stats);
|
||||
}, interval);
|
||||
}
|
||||
|
||||
tick();
|
||||
|
||||
setInterval(tick, interval);
|
@ -5,6 +5,8 @@ import Xev from 'xev';
|
||||
|
||||
const ev = new Xev();
|
||||
|
||||
const interval = 1000;
|
||||
|
||||
/**
|
||||
* Report server stats regularly
|
||||
*/
|
||||
@ -15,7 +17,7 @@ export default function() {
|
||||
ev.emit('serverStatsLog:' + id, log);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
async function tick() {
|
||||
osUtils.cpuUsage(cpuUsage => {
|
||||
const disk = diskusage.checkSync(os.platform() == 'win32' ? 'c:' : '/');
|
||||
const stats = {
|
||||
@ -32,5 +34,9 @@ export default function() {
|
||||
log.push(stats);
|
||||
if (log.length > 50) log.shift();
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
tick();
|
||||
|
||||
setInterval(tick, interval);
|
||||
}
|
@ -12,10 +12,7 @@ const uri = u && p
|
||||
*/
|
||||
import mongo from 'monk';
|
||||
|
||||
const db = mongo(uri, {
|
||||
poolSize: 16,
|
||||
keepAlive: 1
|
||||
});
|
||||
const db = mongo(uri);
|
||||
|
||||
export default db;
|
||||
|
||||
|
@ -17,8 +17,8 @@ import ProgressBar from './utils/cli/progressbar';
|
||||
import EnvironmentInfo from './utils/environmentInfo';
|
||||
import MachineInfo from './utils/machineInfo';
|
||||
import DependencyInfo from './utils/dependencyInfo';
|
||||
import serverStats from './server-stats';
|
||||
import notesStats from './notes-stats';
|
||||
import serverStats from './daemons/server-stats';
|
||||
import notesStats from './daemons/notes-stats';
|
||||
|
||||
import loadConfig from './config/load';
|
||||
import { Config } from './config/types';
|
||||
|
@ -16,6 +16,7 @@ import Following from './following';
|
||||
const Note = db.get<INote>('notes');
|
||||
Note.createIndex('uri', { sparse: true, unique: true });
|
||||
Note.createIndex('userId');
|
||||
Note.createIndex('tags', { sparse: true });
|
||||
Note.createIndex({
|
||||
createdAt: -1
|
||||
});
|
||||
|
@ -48,6 +48,8 @@ type IUserBase = {
|
||||
usernameLower: string;
|
||||
avatarId: mongo.ObjectID;
|
||||
bannerId: mongo.ObjectID;
|
||||
avatarUrl?: string;
|
||||
bannerUrl?: string;
|
||||
wallpaperId: mongo.ObjectID;
|
||||
data: any;
|
||||
description: string;
|
||||
@ -405,13 +407,17 @@ export const pack = (
|
||||
delete _user.publicKey;
|
||||
}
|
||||
|
||||
_user.avatarUrl = _user.avatarId != null
|
||||
? `${config.drive_url}/${_user.avatarId}`
|
||||
: `${config.drive_url}/default-avatar.jpg`;
|
||||
if (_user.avatarUrl == null) {
|
||||
_user.avatarUrl = _user.avatarId != null
|
||||
? `${config.drive_url}/${_user.avatarId}`
|
||||
: `${config.drive_url}/default-avatar.jpg`;
|
||||
}
|
||||
|
||||
_user.bannerUrl = _user.bannerId != null
|
||||
? `${config.drive_url}/${_user.bannerId}`
|
||||
: null;
|
||||
if (_user.bannerUrl == null) {
|
||||
_user.bannerUrl = _user.bannerId != null
|
||||
? `${config.drive_url}/${_user.bannerId}`
|
||||
: null;
|
||||
}
|
||||
|
||||
_user.wallpaperUrl = _user.wallpaperId != null
|
||||
? `${config.drive_url}/${_user.wallpaperId}`
|
||||
|
@ -9,6 +9,7 @@ import webFinger from '../../webfinger';
|
||||
import Resolver from '../resolver';
|
||||
import { resolveImage } from './image';
|
||||
import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type';
|
||||
import { IDriveFile } from '../../../models/drive-file';
|
||||
|
||||
const log = debug('misskey:activitypub');
|
||||
|
||||
@ -117,19 +118,33 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
|
||||
}
|
||||
|
||||
//#region アイコンとヘッダー画像をフェッチ
|
||||
const [avatarId, bannerId] = (await Promise.all([
|
||||
const [avatar, banner] = (await Promise.all<IDriveFile>([
|
||||
person.icon,
|
||||
person.image
|
||||
].map(img =>
|
||||
img == null
|
||||
? Promise.resolve(null)
|
||||
: resolveImage(user, img)
|
||||
))).map(file => file != null ? file._id : null);
|
||||
)));
|
||||
|
||||
User.update({ _id: user._id }, { $set: { avatarId, bannerId } });
|
||||
const avatarId = avatar ? avatar._id : null;
|
||||
const bannerId = banner ? banner._id : null;
|
||||
const avatarUrl = avatar && avatar.metadata.isMetaOnly ? avatar.metadata.url : null;
|
||||
const bannerUrl = banner && banner.metadata.isMetaOnly ? banner.metadata.url : null;
|
||||
|
||||
await User.update({ _id: user._id }, {
|
||||
$set: {
|
||||
avatarId,
|
||||
bannerId,
|
||||
avatarUrl,
|
||||
bannerUrl
|
||||
}
|
||||
});
|
||||
|
||||
user.avatarId = avatarId;
|
||||
user.bannerId = bannerId;
|
||||
user.avatarUrl = avatarUrl;
|
||||
user.bannerUrl = bannerUrl;
|
||||
//#endregion
|
||||
|
||||
return user;
|
||||
@ -190,21 +205,23 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
|
||||
const summaryDOM = JSDOM.fragment(person.summary);
|
||||
|
||||
// アイコンとヘッダー画像をフェッチ
|
||||
const [avatarId, bannerId] = (await Promise.all([
|
||||
const [avatar, banner] = (await Promise.all<IDriveFile>([
|
||||
person.icon,
|
||||
person.image
|
||||
].map(img =>
|
||||
img == null
|
||||
? Promise.resolve(null)
|
||||
: resolveImage(exist, img)
|
||||
))).map(file => file != null ? file._id : null);
|
||||
)));
|
||||
|
||||
// Update user
|
||||
await User.update({ _id: exist._id }, {
|
||||
$set: {
|
||||
updatedAt: new Date(),
|
||||
avatarId,
|
||||
bannerId,
|
||||
avatarId: avatar ? avatar._id : null,
|
||||
bannerId: banner ? banner._id : null,
|
||||
avatarUrl: avatar && avatar.metadata.isMetaOnly ? avatar.metadata.url : null,
|
||||
bannerUrl: banner && banner.metadata.isMetaOnly ? banner.metadata.url : null,
|
||||
description: summaryDOM.textContent,
|
||||
followersCount,
|
||||
followingCount,
|
||||
|
@ -2,6 +2,6 @@ import config from '../../../config';
|
||||
|
||||
export default tag => ({
|
||||
type: 'Hashtag',
|
||||
href: `${config.url}/search?q=#${encodeURIComponent(tag)}`,
|
||||
href: `${config.url}/tags/${encodeURIComponent(tag)}`,
|
||||
name: '#' + tag
|
||||
});
|
||||
|
@ -525,6 +525,9 @@ const endpoints: Endpoint[] = [
|
||||
{
|
||||
name: 'notes/search'
|
||||
},
|
||||
{
|
||||
name: 'notes/search_by_tag'
|
||||
},
|
||||
{
|
||||
name: 'notes/timeline',
|
||||
withCredential: true,
|
||||
@ -625,6 +628,11 @@ const endpoints: Endpoint[] = [
|
||||
withCredential: true
|
||||
},
|
||||
|
||||
{
|
||||
name: 'hashtags/trend',
|
||||
withCredential: true
|
||||
},
|
||||
|
||||
{
|
||||
name: 'messaging/history',
|
||||
withCredential: true,
|
||||
|
136
src/server/api/endpoints/hashtags/trend.ts
Normal file
136
src/server/api/endpoints/hashtags/trend.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import Note from '../../../../models/note';
|
||||
|
||||
/*
|
||||
トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
|
||||
ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる
|
||||
*/
|
||||
|
||||
const rangeA = 1000 * 60 * 30; // 30分
|
||||
const rangeB = 1000 * 60 * 120; // 2時間
|
||||
const coefficient = 1.5; // 「n倍」の部分
|
||||
const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか
|
||||
|
||||
const max = 5;
|
||||
|
||||
/**
|
||||
* Get trends of hashtags
|
||||
*/
|
||||
module.exports = () => new Promise(async (res, rej) => {
|
||||
//#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計
|
||||
const data = await Note.aggregate([{
|
||||
$match: {
|
||||
createdAt: {
|
||||
$gt: new Date(Date.now() - rangeA)
|
||||
},
|
||||
tags: {
|
||||
$exists: true,
|
||||
$ne: []
|
||||
}
|
||||
}
|
||||
}, {
|
||||
$unwind: '$tags'
|
||||
}, {
|
||||
$group: {
|
||||
_id: { tags: '$tags', userId: '$userId' }
|
||||
}
|
||||
}]) as Array<{
|
||||
_id: {
|
||||
tags: string;
|
||||
userId: any;
|
||||
}
|
||||
}>;
|
||||
//#endregion
|
||||
|
||||
if (data.length == 0) {
|
||||
return res([]);
|
||||
}
|
||||
|
||||
const tags = [];
|
||||
|
||||
// カウント
|
||||
data.map(x => x._id).forEach(x => {
|
||||
const i = tags.findIndex(tag => tag.name == x.tags);
|
||||
if (i != -1) {
|
||||
tags[i].count++;
|
||||
} else {
|
||||
tags.push({
|
||||
name: x.tags,
|
||||
count: 1
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 最低要求投稿者数を下回るならカットする
|
||||
const limitedTags = tags.filter(tag => tag.count >= requiredUsers);
|
||||
|
||||
//#region 2. 1で取得したそれぞれのタグについて、「直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上」かどうかを判定する
|
||||
const hotsPromises = limitedTags.map(async tag => {
|
||||
const passedCount = (await Note.distinct('userId', {
|
||||
tags: tag.name,
|
||||
createdAt: {
|
||||
$lt: new Date(Date.now() - rangeA),
|
||||
$gt: new Date(Date.now() - rangeB)
|
||||
}
|
||||
}) as any).length;
|
||||
|
||||
if (tag.count >= (passedCount * coefficient)) {
|
||||
return tag;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
// タグを人気順に並べ替え
|
||||
let hots = (await Promise.all(hotsPromises))
|
||||
.filter(x => x != null)
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.map(tag => tag.name)
|
||||
.slice(0, max);
|
||||
|
||||
//#region 3. もし上記の方法でのトレンド抽出の結果、求められているタグ数に達しなければ「ただ単に現在投稿数が多いハッシュタグ」に切り替える
|
||||
if (hots.length < max) {
|
||||
hots = hots.concat(tags
|
||||
.filter(tag => hots.indexOf(tag.name) == -1)
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.map(tag => tag.name)
|
||||
.slice(0, max - hots.length));
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する
|
||||
const countPromises: Array<Promise<any[]>> = [];
|
||||
|
||||
const range = 20;
|
||||
|
||||
// 10分
|
||||
const interval = 1000 * 60 * 10;
|
||||
|
||||
for (let i = 0; i < range; i++) {
|
||||
countPromises.push(Promise.all(hots.map(tag => Note.distinct('userId', {
|
||||
tags: tag,
|
||||
createdAt: {
|
||||
$lt: new Date(Date.now() - (interval * i)),
|
||||
$gt: new Date(Date.now() - (interval * (i + 1)))
|
||||
}
|
||||
}))));
|
||||
}
|
||||
|
||||
const countsLog = await Promise.all(countPromises);
|
||||
|
||||
const totalCounts: any = await Promise.all(hots.map(tag => Note.distinct('userId', {
|
||||
tags: tag,
|
||||
createdAt: {
|
||||
$gt: new Date(Date.now() - (interval * range))
|
||||
}
|
||||
})));
|
||||
//#endregion
|
||||
|
||||
const stats = hots.map((tag, i) => ({
|
||||
tag,
|
||||
chart: countsLog.map(counts => counts[i].length),
|
||||
usersCount: totalCounts[i].length
|
||||
}));
|
||||
|
||||
res(stats);
|
||||
});
|
329
src/server/api/endpoints/notes/search_by_tag.ts
Normal file
329
src/server/api/endpoints/notes/search_by_tag.ts
Normal file
@ -0,0 +1,329 @@
|
||||
import $ from 'cafy'; import ID from '../../../../cafy-id';
|
||||
import Note from '../../../../models/note';
|
||||
import User from '../../../../models/user';
|
||||
import Mute from '../../../../models/mute';
|
||||
import { getFriendIds } from '../../common/get-friends';
|
||||
import { pack } from '../../../../models/note';
|
||||
|
||||
/**
|
||||
* Search notes by tag
|
||||
*/
|
||||
module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||
// Get 'tag' parameter
|
||||
const [tag, tagError] = $.str.get(params.tag);
|
||||
if (tagError) return rej('invalid tag param');
|
||||
|
||||
// Get 'includeUserIds' parameter
|
||||
const [includeUserIds = [], includeUserIdsErr] = $.arr($.type(ID)).optional().get(params.includeUserIds);
|
||||
if (includeUserIdsErr) return rej('invalid includeUserIds param');
|
||||
|
||||
// Get 'excludeUserIds' parameter
|
||||
const [excludeUserIds = [], excludeUserIdsErr] = $.arr($.type(ID)).optional().get(params.excludeUserIds);
|
||||
if (excludeUserIdsErr) return rej('invalid excludeUserIds param');
|
||||
|
||||
// Get 'includeUserUsernames' parameter
|
||||
const [includeUserUsernames = [], includeUserUsernamesErr] = $.arr($.str).optional().get(params.includeUserUsernames);
|
||||
if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param');
|
||||
|
||||
// Get 'excludeUserUsernames' parameter
|
||||
const [excludeUserUsernames = [], excludeUserUsernamesErr] = $.arr($.str).optional().get(params.excludeUserUsernames);
|
||||
if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param');
|
||||
|
||||
// Get 'following' parameter
|
||||
const [following = null, followingErr] = $.bool.optional().nullable().get(params.following);
|
||||
if (followingErr) return rej('invalid following param');
|
||||
|
||||
// Get 'mute' parameter
|
||||
const [mute = 'mute_all', muteErr] = $.str.optional().get(params.mute);
|
||||
if (muteErr) return rej('invalid mute param');
|
||||
|
||||
// Get 'reply' parameter
|
||||
const [reply = null, replyErr] = $.bool.optional().nullable().get(params.reply);
|
||||
if (replyErr) return rej('invalid reply param');
|
||||
|
||||
// Get 'renote' parameter
|
||||
const [renote = null, renoteErr] = $.bool.optional().nullable().get(params.renote);
|
||||
if (renoteErr) return rej('invalid renote param');
|
||||
|
||||
// Get 'media' parameter
|
||||
const [media = null, mediaErr] = $.bool.optional().nullable().get(params.media);
|
||||
if (mediaErr) return rej('invalid media param');
|
||||
|
||||
// Get 'poll' parameter
|
||||
const [poll = null, pollErr] = $.bool.optional().nullable().get(params.poll);
|
||||
if (pollErr) return rej('invalid poll param');
|
||||
|
||||
// Get 'sinceDate' parameter
|
||||
const [sinceDate, sinceDateErr] = $.num.optional().get(params.sinceDate);
|
||||
if (sinceDateErr) throw 'invalid sinceDate param';
|
||||
|
||||
// Get 'untilDate' parameter
|
||||
const [untilDate, untilDateErr] = $.num.optional().get(params.untilDate);
|
||||
if (untilDateErr) throw 'invalid untilDate param';
|
||||
|
||||
// Get 'offset' parameter
|
||||
const [offset = 0, offsetErr] = $.num.optional().min(0).get(params.offset);
|
||||
if (offsetErr) return rej('invalid offset param');
|
||||
|
||||
// Get 'limit' parameter
|
||||
const [limit = 10, limitErr] = $.num.optional().range(1, 30).get(params.limit);
|
||||
if (limitErr) return rej('invalid limit param');
|
||||
|
||||
let includeUsers = includeUserIds;
|
||||
if (includeUserUsernames != null) {
|
||||
const ids = (await Promise.all(includeUserUsernames.map(async (username) => {
|
||||
const _user = await User.findOne({
|
||||
usernameLower: username.toLowerCase()
|
||||
});
|
||||
return _user ? _user._id : null;
|
||||
}))).filter(id => id != null);
|
||||
includeUsers = includeUsers.concat(ids);
|
||||
}
|
||||
|
||||
let excludeUsers = excludeUserIds;
|
||||
if (excludeUserUsernames != null) {
|
||||
const ids = (await Promise.all(excludeUserUsernames.map(async (username) => {
|
||||
const _user = await User.findOne({
|
||||
usernameLower: username.toLowerCase()
|
||||
});
|
||||
return _user ? _user._id : null;
|
||||
}))).filter(id => id != null);
|
||||
excludeUsers = excludeUsers.concat(ids);
|
||||
}
|
||||
|
||||
search(res, rej, me, tag, includeUsers, excludeUsers, following,
|
||||
mute, reply, renote, media, poll, sinceDate, untilDate, offset, limit);
|
||||
});
|
||||
|
||||
async function search(
|
||||
res, rej, me, tag, includeUserIds, excludeUserIds, following,
|
||||
mute, reply, renote, media, poll, sinceDate, untilDate, offset, max) {
|
||||
|
||||
let q: any = {
|
||||
$and: [{
|
||||
tags: tag
|
||||
}]
|
||||
};
|
||||
|
||||
const push = x => q.$and.push(x);
|
||||
|
||||
if (includeUserIds && includeUserIds.length != 0) {
|
||||
push({
|
||||
userId: {
|
||||
$in: includeUserIds
|
||||
}
|
||||
});
|
||||
} else if (excludeUserIds && excludeUserIds.length != 0) {
|
||||
push({
|
||||
userId: {
|
||||
$nin: excludeUserIds
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (following != null && me != null) {
|
||||
const ids = await getFriendIds(me._id, false);
|
||||
push({
|
||||
userId: following ? {
|
||||
$in: ids
|
||||
} : {
|
||||
$nin: ids.concat(me._id)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (me != null) {
|
||||
const mutes = await Mute.find({
|
||||
muterId: me._id,
|
||||
deletedAt: { $exists: false }
|
||||
});
|
||||
const mutedUserIds = mutes.map(m => m.muteeId);
|
||||
|
||||
switch (mute) {
|
||||
case 'mute_all':
|
||||
push({
|
||||
userId: {
|
||||
$nin: mutedUserIds
|
||||
},
|
||||
'_reply.userId': {
|
||||
$nin: mutedUserIds
|
||||
},
|
||||
'_renote.userId': {
|
||||
$nin: mutedUserIds
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'mute_related':
|
||||
push({
|
||||
'_reply.userId': {
|
||||
$nin: mutedUserIds
|
||||
},
|
||||
'_renote.userId': {
|
||||
$nin: mutedUserIds
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'mute_direct':
|
||||
push({
|
||||
userId: {
|
||||
$nin: mutedUserIds
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'direct_only':
|
||||
push({
|
||||
userId: {
|
||||
$in: mutedUserIds
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'related_only':
|
||||
push({
|
||||
$or: [{
|
||||
'_reply.userId': {
|
||||
$in: mutedUserIds
|
||||
}
|
||||
}, {
|
||||
'_renote.userId': {
|
||||
$in: mutedUserIds
|
||||
}
|
||||
}]
|
||||
});
|
||||
break;
|
||||
case 'all_only':
|
||||
push({
|
||||
$or: [{
|
||||
userId: {
|
||||
$in: mutedUserIds
|
||||
}
|
||||
}, {
|
||||
'_reply.userId': {
|
||||
$in: mutedUserIds
|
||||
}
|
||||
}, {
|
||||
'_renote.userId': {
|
||||
$in: mutedUserIds
|
||||
}
|
||||
}]
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (reply != null) {
|
||||
if (reply) {
|
||||
push({
|
||||
replyId: {
|
||||
$exists: true,
|
||||
$ne: null
|
||||
}
|
||||
});
|
||||
} else {
|
||||
push({
|
||||
$or: [{
|
||||
replyId: {
|
||||
$exists: false
|
||||
}
|
||||
}, {
|
||||
replyId: null
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (renote != null) {
|
||||
if (renote) {
|
||||
push({
|
||||
renoteId: {
|
||||
$exists: true,
|
||||
$ne: null
|
||||
}
|
||||
});
|
||||
} else {
|
||||
push({
|
||||
$or: [{
|
||||
renoteId: {
|
||||
$exists: false
|
||||
}
|
||||
}, {
|
||||
renoteId: null
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (media != null) {
|
||||
if (media) {
|
||||
push({
|
||||
mediaIds: {
|
||||
$exists: true,
|
||||
$ne: null
|
||||
}
|
||||
});
|
||||
} else {
|
||||
push({
|
||||
$or: [{
|
||||
mediaIds: {
|
||||
$exists: false
|
||||
}
|
||||
}, {
|
||||
mediaIds: null
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (poll != null) {
|
||||
if (poll) {
|
||||
push({
|
||||
poll: {
|
||||
$exists: true,
|
||||
$ne: null
|
||||
}
|
||||
});
|
||||
} else {
|
||||
push({
|
||||
$or: [{
|
||||
poll: {
|
||||
$exists: false
|
||||
}
|
||||
}, {
|
||||
poll: null
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (sinceDate) {
|
||||
push({
|
||||
createdAt: {
|
||||
$gt: new Date(sinceDate)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (untilDate) {
|
||||
push({
|
||||
createdAt: {
|
||||
$lt: new Date(untilDate)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (q.$and.length == 0) {
|
||||
q = {};
|
||||
}
|
||||
|
||||
// Search notes
|
||||
const notes = await Note
|
||||
.find(q, {
|
||||
sort: {
|
||||
_id: -1
|
||||
},
|
||||
limit: max,
|
||||
skip: offset
|
||||
});
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(notes.map(note => pack(note, me))));
|
||||
}
|
@ -94,7 +94,7 @@ export default async (user: IUser, data: {
|
||||
if (data.visibility == null) data.visibility = 'public';
|
||||
if (data.viaMobile == null) data.viaMobile = false;
|
||||
|
||||
const tags = data.tags || [];
|
||||
let tags = data.tags || [];
|
||||
|
||||
let tokens: any[] = null;
|
||||
|
||||
@ -114,6 +114,8 @@ export default async (user: IUser, data: {
|
||||
});
|
||||
}
|
||||
|
||||
tags = tags.filter(tag => tag.length <= 100);
|
||||
|
||||
if (data.visibleUsers) {
|
||||
data.visibleUsers = data.visibleUsers.filter(x => x != null);
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ const handlers = {
|
||||
|
||||
hashtag({ document }, { hashtag }) {
|
||||
const a = document.createElement('a');
|
||||
a.href = config.url + '/search?q=#' + hashtag;
|
||||
a.href = config.url + '/tags/' + hashtag;
|
||||
a.textContent = '#' + hashtag;
|
||||
a.setAttribute('rel', 'tag');
|
||||
document.body.appendChild(a);
|
||||
|
Reference in New Issue
Block a user