Compare commits
163 Commits
Author | SHA1 | Date | |
---|---|---|---|
792632d726 | |||
9cac293efc | |||
cd8bfca29c | |||
b5b437b878 | |||
cc2947063a | |||
2864a9027f | |||
e11f547308 | |||
f164661ef2 | |||
c9d993b838 | |||
65f35dc9f4 | |||
b600d462c1 | |||
fa5a82c9ab | |||
7c596be638 | |||
07265f594b | |||
392cb1ba89 | |||
e6f33e997f | |||
a44387f250 | |||
b1b1b7592b | |||
ca668898f4 | |||
fcd437c89f | |||
7f7d7edc7f | |||
bd827f946a | |||
ad8aa1c179 | |||
3ebaf83ce0 | |||
39b1978ff3 | |||
bddff17e5e | |||
0ac9120064 | |||
d90f75425f | |||
dec7d537dc | |||
11e95ea092 | |||
c5e9b69eb3 | |||
120c11b181 | |||
a1ae832129 | |||
3a4833818f | |||
8814fc9c9c | |||
e6e02ece89 | |||
9059c149dd | |||
7d8e70b2ac | |||
89105f5641 | |||
1813d17b4c | |||
ce27b36fd0 | |||
e635a87628 | |||
80c52433cc | |||
1472f0b141 | |||
4d914f5c0a | |||
0318f7344f | |||
413fbb3d0c | |||
8bc47baf4f | |||
e3f6d42a47 | |||
8230935fd3 | |||
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 | |||
c8dd8341ca | |||
8bcf44bc16 | |||
50b37a8420 | |||
22df795733 |
@ -1,3 +1,9 @@
|
|||||||
|
# インスタンス名
|
||||||
|
name:
|
||||||
|
|
||||||
|
# インスタンスの紹介
|
||||||
|
description:
|
||||||
|
|
||||||
# サーバーのメンテナ情報
|
# サーバーのメンテナ情報
|
||||||
maintainer:
|
maintainer:
|
||||||
# メンテナの名前
|
# メンテナの名前
|
||||||
@ -55,3 +61,7 @@ twitter:
|
|||||||
|
|
||||||
# インテグレーション用アプリのコンシューマーシークレット
|
# インテグレーション用アプリのコンシューマーシークレット
|
||||||
consumer_secret:
|
consumer_secret:
|
||||||
|
|
||||||
|
# true にすると、リモートのファイルをキャッシュしなくなります(直リンクします)。
|
||||||
|
# ストレージ容量を節約することができますが、「リモートメディアを表示しない」設定をオンにしているユーザーは、リモートの画像などは見えなくなります。
|
||||||
|
preventCache: false
|
||||||
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1,3 +1,4 @@
|
|||||||
*.svg -diff -text
|
*.svg -diff -text
|
||||||
*.psd -diff -text
|
*.psd -diff -text
|
||||||
*.ai -diff -text
|
*.ai -diff -text
|
||||||
|
yarn.lock -diff -text
|
||||||
|
11
CHANGELOG.md
Normal file
11
CHANGELOG.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
ChangeLog
|
||||||
|
=========
|
||||||
|
|
||||||
|
3.0.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
起動する前に、`node cli/recount-stats`してください。
|
||||||
|
|
||||||
|
Please run `node cli/recount-stats` before launch.
|
42
cli/recount-stats.js
Normal file
42
cli/recount-stats.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
const { default: Note } = require('../built/models/note');
|
||||||
|
const { default: Meta } = require('../built/models/meta');
|
||||||
|
const { default: User } = require('../built/models/user');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const meta = await Meta.findOne({});
|
||||||
|
|
||||||
|
const notesCount = await Note.count();
|
||||||
|
|
||||||
|
const usersCount = await User.count();
|
||||||
|
|
||||||
|
const originalNotesCount = await Note.count({
|
||||||
|
'_user.host': null
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalUsersCount = await User.count({
|
||||||
|
host: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
notesCount,
|
||||||
|
usersCount,
|
||||||
|
originalNotesCount,
|
||||||
|
originalUsersCount
|
||||||
|
};
|
||||||
|
|
||||||
|
if (meta) {
|
||||||
|
await Meta.update({}, {
|
||||||
|
$set: {
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await Meta.insert({
|
||||||
|
stats
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().then(() => {
|
||||||
|
console.log('done');
|
||||||
|
}).catch(console.error);
|
@ -70,6 +70,7 @@ common:
|
|||||||
donation: "Spenden"
|
donation: "Spenden"
|
||||||
nav: "Navigation"
|
nav: "Navigation"
|
||||||
tips: "Tipps"
|
tips: "Tipps"
|
||||||
|
hashtags: "ハッシュタグ"
|
||||||
deck:
|
deck:
|
||||||
widgets: "Widget hinzufügen:"
|
widgets: "Widget hinzufügen:"
|
||||||
home: "Startseite"
|
home: "Startseite"
|
||||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
|||||||
common/views/widgets/posts-monitor.vue:
|
common/views/widgets/posts-monitor.vue:
|
||||||
title: "投稿チャート"
|
title: "投稿チャート"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
|
common/views/widgets/hashtags.vue:
|
||||||
|
title: "ハッシュタグ"
|
||||||
|
count: "{}人が投稿"
|
||||||
common/views/widgets/server.vue:
|
common/views/widgets/server.vue:
|
||||||
title: "Serverinformationen"
|
title: "Serverinformationen"
|
||||||
toggle: "Sicht umschalten"
|
toggle: "Sicht umschalten"
|
||||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
|||||||
desktop/views/pages/deck/deck.tl-column.vue:
|
desktop/views/pages/deck/deck.tl-column.vue:
|
||||||
is-media-only: "メディア投稿のみ"
|
is-media-only: "メディア投稿のみ"
|
||||||
is-media-view: "メディアビュー"
|
is-media-view: "メディアビュー"
|
||||||
|
desktop/views/pages/deck/deck.note.vue:
|
||||||
|
reposted-by: "{}がRenote"
|
||||||
|
private: "この投稿は非公開です"
|
||||||
|
deleted: "この投稿は削除されました"
|
||||||
desktop/views/pages/welcome.vue:
|
desktop/views/pages/welcome.vue:
|
||||||
about: "詳しく..."
|
about: "詳しく..."
|
||||||
gotit: "わかった"
|
gotit: "わかった"
|
||||||
|
@ -70,6 +70,7 @@ common:
|
|||||||
donation: "Donation"
|
donation: "Donation"
|
||||||
nav: "Navigation"
|
nav: "Navigation"
|
||||||
tips: "Tips"
|
tips: "Tips"
|
||||||
|
hashtags: "Hashtags"
|
||||||
deck:
|
deck:
|
||||||
widgets: "Widgets"
|
widgets: "Widgets"
|
||||||
home: "Home"
|
home: "Home"
|
||||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
|||||||
common/views/widgets/posts-monitor.vue:
|
common/views/widgets/posts-monitor.vue:
|
||||||
title: "Chart of posts"
|
title: "Chart of posts"
|
||||||
toggle: "Toggle views"
|
toggle: "Toggle views"
|
||||||
|
common/views/widgets/hashtags.vue:
|
||||||
|
title: "Hashtags"
|
||||||
|
count: "{} users mentioned"
|
||||||
common/views/widgets/server.vue:
|
common/views/widgets/server.vue:
|
||||||
title: "Server info"
|
title: "Server info"
|
||||||
toggle: "Toggle views"
|
toggle: "Toggle views"
|
||||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
|||||||
desktop/views/pages/deck/deck.tl-column.vue:
|
desktop/views/pages/deck/deck.tl-column.vue:
|
||||||
is-media-only: "Only media posts"
|
is-media-only: "Only media posts"
|
||||||
is-media-view: "Media view"
|
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:
|
desktop/views/pages/welcome.vue:
|
||||||
about: "about"
|
about: "about"
|
||||||
gotit: "Got it!"
|
gotit: "Got it!"
|
||||||
|
@ -70,6 +70,7 @@ common:
|
|||||||
donation: "寄付のお願い"
|
donation: "寄付のお願い"
|
||||||
nav: "ナビゲーション"
|
nav: "ナビゲーション"
|
||||||
tips: "ヒント"
|
tips: "ヒント"
|
||||||
|
hashtags: "ハッシュタグ"
|
||||||
deck:
|
deck:
|
||||||
widgets: "ウィジェット"
|
widgets: "ウィジェット"
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
|||||||
common/views/widgets/posts-monitor.vue:
|
common/views/widgets/posts-monitor.vue:
|
||||||
title: "投稿チャート"
|
title: "投稿チャート"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
|
common/views/widgets/hashtags.vue:
|
||||||
|
title: "ハッシュタグ"
|
||||||
|
count: "{}人が投稿"
|
||||||
common/views/widgets/server.vue:
|
common/views/widgets/server.vue:
|
||||||
title: "サーバー情報"
|
title: "サーバー情報"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
|||||||
desktop/views/pages/deck/deck.tl-column.vue:
|
desktop/views/pages/deck/deck.tl-column.vue:
|
||||||
is-media-only: "メディア投稿のみ"
|
is-media-only: "メディア投稿のみ"
|
||||||
is-media-view: "メディアビュー"
|
is-media-view: "メディアビュー"
|
||||||
|
desktop/views/pages/deck/deck.note.vue:
|
||||||
|
reposted-by: "{}がRenote"
|
||||||
|
private: "この投稿は非公開です"
|
||||||
|
deleted: "この投稿は削除されました"
|
||||||
desktop/views/pages/welcome.vue:
|
desktop/views/pages/welcome.vue:
|
||||||
about: "詳しく..."
|
about: "詳しく..."
|
||||||
gotit: "わかった"
|
gotit: "わかった"
|
||||||
|
@ -54,22 +54,23 @@ common:
|
|||||||
timemachine: "カレンダー(タイムマシン)"
|
timemachine: "カレンダー(タイムマシン)"
|
||||||
activity: "Activité"
|
activity: "Activité"
|
||||||
rss: "Lecteur de flux RSS"
|
rss: "Lecteur de flux RSS"
|
||||||
memo: "付箋"
|
memo: "Pense-bête"
|
||||||
trends: "Tendances"
|
trends: "Tendances"
|
||||||
photo-stream: "Flux de photos"
|
photo-stream: "Flux de photos"
|
||||||
posts-monitor: "投稿チャート"
|
posts-monitor: "Graph des publications"
|
||||||
slideshow: "Diaporama"
|
slideshow: "Diaporama"
|
||||||
version: "Version"
|
version: "Version"
|
||||||
broadcast: "Diffusion"
|
broadcast: "Diffusion"
|
||||||
notifications: "Notifications"
|
notifications: "Notifications"
|
||||||
users: "Utilisateurs"
|
users: "Utilisateurs"
|
||||||
polls: "アンケート"
|
polls: "Sondages"
|
||||||
post-form: "投稿フォーム"
|
post-form: "投稿フォーム"
|
||||||
messaging: "Messagerie"
|
messaging: "Messagerie"
|
||||||
server: "Info sur le serveur"
|
server: "Info sur le serveur"
|
||||||
donation: "Dons"
|
donation: "Dons"
|
||||||
nav: "Navigation"
|
nav: "Navigation"
|
||||||
tips: "Conseils"
|
tips: "Conseils"
|
||||||
|
hashtags: "ハッシュタグ"
|
||||||
deck:
|
deck:
|
||||||
widgets: "Widgets"
|
widgets: "Widgets"
|
||||||
home: "Accueil"
|
home: "Accueil"
|
||||||
@ -151,11 +152,11 @@ common/views/components/poll.vue:
|
|||||||
show-result: "Montrer les résultats"
|
show-result: "Montrer les résultats"
|
||||||
voted: "Voté"
|
voted: "Voté"
|
||||||
common/views/components/poll-editor.vue:
|
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 {}"
|
choice-n: "Choix {}"
|
||||||
remove: "Supprimer ce choix"
|
remove: "Supprimer ce choix"
|
||||||
add: "+ Ajouter un choix"
|
add: "+ Ajouter un choix"
|
||||||
destroy: "アンケートを破棄"
|
destroy: "Annuler ce sondage"
|
||||||
common/views/components/reaction-picker.vue:
|
common/views/components/reaction-picker.vue:
|
||||||
choose-reaction: "Choisissez votre réaction"
|
choose-reaction: "Choisissez votre réaction"
|
||||||
common/views/components/signin.vue:
|
common/views/components/signin.vue:
|
||||||
@ -222,13 +223,16 @@ common/views/widgets/photo-stream.vue:
|
|||||||
title: "Flux de photo"
|
title: "Flux de photo"
|
||||||
no-photos: "Pas de photos"
|
no-photos: "Pas de photos"
|
||||||
common/views/widgets/posts-monitor.vue:
|
common/views/widgets/posts-monitor.vue:
|
||||||
title: "投稿チャート"
|
title: "Graph des publications"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
|
common/views/widgets/hashtags.vue:
|
||||||
|
title: "ハッシュタグ"
|
||||||
|
count: "{}人が投稿"
|
||||||
common/views/widgets/server.vue:
|
common/views/widgets/server.vue:
|
||||||
title: "Info sur le serveur"
|
title: "Info sur le serveur"
|
||||||
toggle: "Afficher les vues"
|
toggle: "Afficher les vues"
|
||||||
common/views/widgets/memo.vue:
|
common/views/widgets/memo.vue:
|
||||||
title: "付箋"
|
title: "Pense-bête"
|
||||||
memo: "Écrivez ici !"
|
memo: "Écrivez ici !"
|
||||||
save: "Enregistrer"
|
save: "Enregistrer"
|
||||||
desktop/views/components/activity.chart.vue:
|
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-media-from-drive: "Joindre un media depuis votre Drive"
|
||||||
attach-cancel: "Annuler la jointure de fichier"
|
attach-cancel: "Annuler la jointure de fichier"
|
||||||
insert-a-kao: "v(‘ω’)v"
|
insert-a-kao: "v(‘ω’)v"
|
||||||
create-poll: "アンケートを作成"
|
create-poll: "Créer un sondage"
|
||||||
text-remain: "{} charactères restants"
|
text-remain: "{} charactères restants"
|
||||||
desktop/views/components/post-form-window.vue:
|
desktop/views/components/post-form-window.vue:
|
||||||
note: "Nouvelle note"
|
note: "Nouvelle note"
|
||||||
@ -414,7 +418,7 @@ desktop/views/components/settings.vue:
|
|||||||
license: "License"
|
license: "License"
|
||||||
behaviour: "Comportement"
|
behaviour: "Comportement"
|
||||||
fetch-on-scroll: "Chargement lors du défilement"
|
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: "Fenêtre contextuelle automatique"
|
||||||
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
|
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
|
||||||
advanced: "Paramètres avancés"
|
advanced: "Paramètres avancés"
|
||||||
@ -523,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
|
|||||||
private: "cette publication est privée"
|
private: "cette publication est privée"
|
||||||
deleted: "cette publication a été supprimée"
|
deleted: "cette publication a été supprimée"
|
||||||
media-count: "{} médias attachés"
|
media-count: "{} médias attachés"
|
||||||
poll: "アンケート"
|
poll: "Sondage"
|
||||||
desktop/views/components/taskmanager.vue:
|
desktop/views/components/taskmanager.vue:
|
||||||
title: "Gestionnaire de tâches"
|
title: "Gestionnaire de tâches"
|
||||||
desktop/views/components/timeline.vue:
|
desktop/views/components/timeline.vue:
|
||||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
|||||||
desktop/views/pages/deck/deck.tl-column.vue:
|
desktop/views/pages/deck/deck.tl-column.vue:
|
||||||
is-media-only: "メディア投稿のみ"
|
is-media-only: "メディア投稿のみ"
|
||||||
is-media-view: "メディアビュー"
|
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:
|
desktop/views/pages/welcome.vue:
|
||||||
about: "à propos"
|
about: "à propos"
|
||||||
gotit: "J'ai compris !"
|
gotit: "J'ai compris !"
|
||||||
@ -639,7 +647,7 @@ desktop/views/widgets/notifications.vue:
|
|||||||
title: "Notifications"
|
title: "Notifications"
|
||||||
settings: "Réglages"
|
settings: "Réglages"
|
||||||
desktop/views/widgets/polls.vue:
|
desktop/views/widgets/polls.vue:
|
||||||
title: "アンケート"
|
title: "Sondages"
|
||||||
refresh: "Afficher d'autres"
|
refresh: "Afficher d'autres"
|
||||||
nothing: "Rien"
|
nothing: "Rien"
|
||||||
desktop/views/widgets/post-form.vue:
|
desktop/views/widgets/post-form.vue:
|
||||||
@ -666,7 +674,7 @@ mobile/views/components/drive.vue:
|
|||||||
nothing-in-drive: "Rien"
|
nothing-in-drive: "Rien"
|
||||||
folder-is-empty: "Ce dossier est vide"
|
folder-is-empty: "Ce dossier est vide"
|
||||||
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
|
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"
|
folder-name: "Nom du dossier"
|
||||||
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
|
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
|
||||||
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
|
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
|
||||||
|
@ -70,6 +70,7 @@ common:
|
|||||||
donation: "寄付のお願い"
|
donation: "寄付のお願い"
|
||||||
nav: "ナビゲーション"
|
nav: "ナビゲーション"
|
||||||
tips: "ヒント"
|
tips: "ヒント"
|
||||||
|
hashtags: "ハッシュタグ"
|
||||||
deck:
|
deck:
|
||||||
widgets: "ウィジェット"
|
widgets: "ウィジェット"
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
|||||||
common/views/widgets/posts-monitor.vue:
|
common/views/widgets/posts-monitor.vue:
|
||||||
title: "投稿チャート"
|
title: "投稿チャート"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
|
common/views/widgets/hashtags.vue:
|
||||||
|
title: "ハッシュタグ"
|
||||||
|
count: "{}人が投稿"
|
||||||
common/views/widgets/server.vue:
|
common/views/widgets/server.vue:
|
||||||
title: "サーバー情報"
|
title: "サーバー情報"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
|||||||
desktop/views/pages/deck/deck.tl-column.vue:
|
desktop/views/pages/deck/deck.tl-column.vue:
|
||||||
is-media-only: "メディア投稿のみ"
|
is-media-only: "メディア投稿のみ"
|
||||||
is-media-view: "メディアビュー"
|
is-media-view: "メディアビュー"
|
||||||
|
desktop/views/pages/deck/deck.note.vue:
|
||||||
|
reposted-by: "{}がRenote"
|
||||||
|
private: "この投稿は非公開です"
|
||||||
|
deleted: "この投稿は削除されました"
|
||||||
desktop/views/pages/welcome.vue:
|
desktop/views/pages/welcome.vue:
|
||||||
about: "詳しく..."
|
about: "詳しく..."
|
||||||
gotit: "わかった"
|
gotit: "わかった"
|
||||||
|
@ -3,7 +3,7 @@ meta:
|
|||||||
divider: ""
|
divider: ""
|
||||||
|
|
||||||
common:
|
common:
|
||||||
misskey: "A planet of fediverse"
|
misskey: "A ⭐ of fediverse"
|
||||||
about-title: "A ⭐ of fediverse."
|
about-title: "A ⭐ of fediverse."
|
||||||
about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。"
|
about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。"
|
||||||
|
|
||||||
@ -76,6 +76,7 @@ common:
|
|||||||
donation: "寄付のお願い"
|
donation: "寄付のお願い"
|
||||||
nav: "ナビゲーション"
|
nav: "ナビゲーション"
|
||||||
tips: "ヒント"
|
tips: "ヒント"
|
||||||
|
hashtags: "ハッシュタグ"
|
||||||
|
|
||||||
deck:
|
deck:
|
||||||
widgets: "ウィジェット"
|
widgets: "ウィジェット"
|
||||||
@ -254,6 +255,11 @@ common/views/widgets/posts-monitor.vue:
|
|||||||
title: "投稿チャート"
|
title: "投稿チャート"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
|
|
||||||
|
common/views/widgets/hashtags.vue:
|
||||||
|
title: "ハッシュタグ"
|
||||||
|
count: "{}人が投稿"
|
||||||
|
empty: "トレンドなし"
|
||||||
|
|
||||||
common/views/widgets/server.vue:
|
common/views/widgets/server.vue:
|
||||||
title: "サーバー情報"
|
title: "サーバー情報"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
@ -672,6 +678,11 @@ desktop/views/pages/deck/deck.tl-column.vue:
|
|||||||
is-media-only: "メディア投稿のみ"
|
is-media-only: "メディア投稿のみ"
|
||||||
is-media-view: "メディアビュー"
|
is-media-view: "メディアビュー"
|
||||||
|
|
||||||
|
desktop/views/pages/deck/deck.note.vue:
|
||||||
|
reposted-by: "{}がRenote"
|
||||||
|
private: "この投稿は非公開です"
|
||||||
|
deleted: "この投稿は削除されました"
|
||||||
|
|
||||||
desktop/views/pages/welcome.vue:
|
desktop/views/pages/welcome.vue:
|
||||||
about: "詳しく..."
|
about: "詳しく..."
|
||||||
gotit: "わかった"
|
gotit: "わかった"
|
||||||
|
@ -70,6 +70,7 @@ common:
|
|||||||
donation: "寄付のお願い"
|
donation: "寄付のお願い"
|
||||||
nav: "ナビゲーション"
|
nav: "ナビゲーション"
|
||||||
tips: "ヒント"
|
tips: "ヒント"
|
||||||
|
hashtags: "ハッシュタグ"
|
||||||
deck:
|
deck:
|
||||||
widgets: "ウィジェット"
|
widgets: "ウィジェット"
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
|||||||
common/views/widgets/posts-monitor.vue:
|
common/views/widgets/posts-monitor.vue:
|
||||||
title: "投稿チャート"
|
title: "投稿チャート"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
|
common/views/widgets/hashtags.vue:
|
||||||
|
title: "ハッシュタグ"
|
||||||
|
count: "{}人が投稿"
|
||||||
common/views/widgets/server.vue:
|
common/views/widgets/server.vue:
|
||||||
title: "サーバー情報"
|
title: "サーバー情報"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
|||||||
desktop/views/pages/deck/deck.tl-column.vue:
|
desktop/views/pages/deck/deck.tl-column.vue:
|
||||||
is-media-only: "メディア投稿のみ"
|
is-media-only: "メディア投稿のみ"
|
||||||
is-media-view: "メディアビュー"
|
is-media-view: "メディアビュー"
|
||||||
|
desktop/views/pages/deck/deck.note.vue:
|
||||||
|
reposted-by: "{}がRenote"
|
||||||
|
private: "この投稿は非公開です"
|
||||||
|
deleted: "この投稿は削除されました"
|
||||||
desktop/views/pages/welcome.vue:
|
desktop/views/pages/welcome.vue:
|
||||||
about: "詳しく..."
|
about: "詳しく..."
|
||||||
gotit: "わかった"
|
gotit: "わかった"
|
||||||
|
@ -54,7 +54,7 @@ common:
|
|||||||
timemachine: "Kalendarz (wehikuł czasu)"
|
timemachine: "Kalendarz (wehikuł czasu)"
|
||||||
activity: "Aktywność"
|
activity: "Aktywność"
|
||||||
rss: "Czytnik RSS"
|
rss: "Czytnik RSS"
|
||||||
memo: "付箋"
|
memo: "Notatka"
|
||||||
trends: "Na czasie"
|
trends: "Na czasie"
|
||||||
photo-stream: "Photostream"
|
photo-stream: "Photostream"
|
||||||
posts-monitor: "Wykres wpisów"
|
posts-monitor: "Wykres wpisów"
|
||||||
@ -63,13 +63,14 @@ common:
|
|||||||
broadcast: "Transmisja"
|
broadcast: "Transmisja"
|
||||||
notifications: "Powiadomienia"
|
notifications: "Powiadomienia"
|
||||||
users: "Polecani użytkownicy"
|
users: "Polecani użytkownicy"
|
||||||
polls: "アンケート"
|
polls: "Ankiety"
|
||||||
post-form: "Formularz tworzenia"
|
post-form: "Formularz tworzenia"
|
||||||
messaging: "Wiadomości"
|
messaging: "Wiadomości"
|
||||||
server: "Informacje o serwerze"
|
server: "Informacje o serwerze"
|
||||||
donation: "Dotacje"
|
donation: "Dotacje"
|
||||||
nav: "Nawigacja"
|
nav: "Nawigacja"
|
||||||
tips: "Wskazówki"
|
tips: "Wskazówki"
|
||||||
|
hashtags: "Hashtagi"
|
||||||
deck:
|
deck:
|
||||||
widgets: "Widżety"
|
widgets: "Widżety"
|
||||||
home: "Strona główna"
|
home: "Strona główna"
|
||||||
@ -151,11 +152,11 @@ common/views/components/poll.vue:
|
|||||||
show-result: "Pokaż wyniki"
|
show-result: "Pokaż wyniki"
|
||||||
voted: "Zagłosowano"
|
voted: "Zagłosowano"
|
||||||
common/views/components/poll-editor.vue:
|
common/views/components/poll-editor.vue:
|
||||||
no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
|
no-only-one-choice: "Musisz wprowadzić przynajmniej dwie opcje."
|
||||||
choice-n: "Opcja {}"
|
choice-n: "Opcja {}"
|
||||||
remove: "Usuń tą opcję"
|
remove: "Usuń tą opcję"
|
||||||
add: "+ Dodaj opcję"
|
add: "+ Dodaj opcję"
|
||||||
destroy: "アンケートを破棄"
|
destroy: "Usuń tę ankietę"
|
||||||
common/views/components/reaction-picker.vue:
|
common/views/components/reaction-picker.vue:
|
||||||
choose-reaction: "Wybierz reakcję"
|
choose-reaction: "Wybierz reakcję"
|
||||||
common/views/components/signin.vue:
|
common/views/components/signin.vue:
|
||||||
@ -224,11 +225,14 @@ common/views/widgets/photo-stream.vue:
|
|||||||
common/views/widgets/posts-monitor.vue:
|
common/views/widgets/posts-monitor.vue:
|
||||||
title: "Wykres wpisów"
|
title: "Wykres wpisów"
|
||||||
toggle: "Przełącz widok"
|
toggle: "Przełącz widok"
|
||||||
|
common/views/widgets/hashtags.vue:
|
||||||
|
title: "Hashtagi"
|
||||||
|
count: "Wspomniany przez {} użytkowników"
|
||||||
common/views/widgets/server.vue:
|
common/views/widgets/server.vue:
|
||||||
title: "Informacje o serwerze"
|
title: "Informacje o serwerze"
|
||||||
toggle: "Przełącz widok"
|
toggle: "Przełącz widok"
|
||||||
common/views/widgets/memo.vue:
|
common/views/widgets/memo.vue:
|
||||||
title: "付箋"
|
title: "Notatka"
|
||||||
memo: "Napisz tutaj!"
|
memo: "Napisz tutaj!"
|
||||||
save: "Zapisz"
|
save: "Zapisz"
|
||||||
desktop/views/components/activity.chart.vue:
|
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-media-from-drive: "Załącz zawartość multimedialną z dysku"
|
||||||
attach-cancel: "Usuń załącznik"
|
attach-cancel: "Usuń załącznik"
|
||||||
insert-a-kao: "v(‘ω’)v"
|
insert-a-kao: "v(‘ω’)v"
|
||||||
create-poll: "アンケートを作成"
|
create-poll: "Utwórz ankietę"
|
||||||
text-remain: "pozostałe znaki: {}"
|
text-remain: "pozostałe znaki: {}"
|
||||||
desktop/views/components/post-form-window.vue:
|
desktop/views/components/post-form-window.vue:
|
||||||
note: "Nowy wpis"
|
note: "Nowy wpis"
|
||||||
@ -523,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
|
|||||||
private: "ten wpis jest prywatny"
|
private: "ten wpis jest prywatny"
|
||||||
deleted: "ten wpis został usunięty"
|
deleted: "ten wpis został usunięty"
|
||||||
media-count: "{}zawartości multimedialnej"
|
media-count: "{}zawartości multimedialnej"
|
||||||
poll: "アンケート"
|
poll: "Ankieta"
|
||||||
desktop/views/components/taskmanager.vue:
|
desktop/views/components/taskmanager.vue:
|
||||||
title: "Menedżer zadań"
|
title: "Menedżer zadań"
|
||||||
desktop/views/components/timeline.vue:
|
desktop/views/components/timeline.vue:
|
||||||
@ -574,8 +578,12 @@ desktop/views/components/window.vue:
|
|||||||
popout: "Pop-out"
|
popout: "Pop-out"
|
||||||
close: "Zamknij"
|
close: "Zamknij"
|
||||||
desktop/views/pages/deck/deck.tl-column.vue:
|
desktop/views/pages/deck/deck.tl-column.vue:
|
||||||
is-media-only: "メディア投稿のみ"
|
is-media-only: "Tylko wpisy z zawartością multimedialną"
|
||||||
is-media-view: "メディアビュー"
|
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:
|
desktop/views/pages/welcome.vue:
|
||||||
about: "O Misskey"
|
about: "O Misskey"
|
||||||
gotit: "Rozumiem!"
|
gotit: "Rozumiem!"
|
||||||
@ -639,7 +647,7 @@ desktop/views/widgets/notifications.vue:
|
|||||||
title: "Powiadomienia"
|
title: "Powiadomienia"
|
||||||
settings: "Ustawienia"
|
settings: "Ustawienia"
|
||||||
desktop/views/widgets/polls.vue:
|
desktop/views/widgets/polls.vue:
|
||||||
title: "アンケート"
|
title: "Ankiety"
|
||||||
refresh: "Pokaż inne"
|
refresh: "Pokaż inne"
|
||||||
nothing: "Pusto"
|
nothing: "Pusto"
|
||||||
desktop/views/widgets/post-form.vue:
|
desktop/views/widgets/post-form.vue:
|
||||||
@ -738,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
|
|||||||
private: "ten wpis jest prywatny"
|
private: "ten wpis jest prywatny"
|
||||||
deleted: "ten wpis został usunięty"
|
deleted: "ten wpis został usunięty"
|
||||||
media-count: "{}zawartości multimedialnej"
|
media-count: "{}zawartości multimedialnej"
|
||||||
poll: "アンケート"
|
poll: "Ankieta"
|
||||||
mobile/views/components/timeline.vue:
|
mobile/views/components/timeline.vue:
|
||||||
empty: "Brak wpisów"
|
empty: "Brak wpisów"
|
||||||
load-more: "Więcej"
|
load-more: "Więcej"
|
||||||
|
@ -70,6 +70,7 @@ common:
|
|||||||
donation: "寄付のお願い"
|
donation: "寄付のお願い"
|
||||||
nav: "ナビゲーション"
|
nav: "ナビゲーション"
|
||||||
tips: "ヒント"
|
tips: "ヒント"
|
||||||
|
hashtags: "ハッシュタグ"
|
||||||
deck:
|
deck:
|
||||||
widgets: "ウィジェット"
|
widgets: "ウィジェット"
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
|||||||
common/views/widgets/posts-monitor.vue:
|
common/views/widgets/posts-monitor.vue:
|
||||||
title: "投稿チャート"
|
title: "投稿チャート"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
|
common/views/widgets/hashtags.vue:
|
||||||
|
title: "ハッシュタグ"
|
||||||
|
count: "{}人が投稿"
|
||||||
common/views/widgets/server.vue:
|
common/views/widgets/server.vue:
|
||||||
title: "サーバー情報"
|
title: "サーバー情報"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
|||||||
desktop/views/pages/deck/deck.tl-column.vue:
|
desktop/views/pages/deck/deck.tl-column.vue:
|
||||||
is-media-only: "メディア投稿のみ"
|
is-media-only: "メディア投稿のみ"
|
||||||
is-media-view: "メディアビュー"
|
is-media-view: "メディアビュー"
|
||||||
|
desktop/views/pages/deck/deck.note.vue:
|
||||||
|
reposted-by: "{}がRenote"
|
||||||
|
private: "この投稿は非公開です"
|
||||||
|
deleted: "この投稿は削除されました"
|
||||||
desktop/views/pages/welcome.vue:
|
desktop/views/pages/welcome.vue:
|
||||||
about: "詳しく..."
|
about: "詳しく..."
|
||||||
gotit: "わかった"
|
gotit: "わかった"
|
||||||
|
@ -70,6 +70,7 @@ common:
|
|||||||
donation: "寄付のお願い"
|
donation: "寄付のお願い"
|
||||||
nav: "ナビゲーション"
|
nav: "ナビゲーション"
|
||||||
tips: "ヒント"
|
tips: "ヒント"
|
||||||
|
hashtags: "ハッシュタグ"
|
||||||
deck:
|
deck:
|
||||||
widgets: "ウィジェット"
|
widgets: "ウィジェット"
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
|||||||
common/views/widgets/posts-monitor.vue:
|
common/views/widgets/posts-monitor.vue:
|
||||||
title: "投稿チャート"
|
title: "投稿チャート"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
|
common/views/widgets/hashtags.vue:
|
||||||
|
title: "ハッシュタグ"
|
||||||
|
count: "{}人が投稿"
|
||||||
common/views/widgets/server.vue:
|
common/views/widgets/server.vue:
|
||||||
title: "サーバー情報"
|
title: "サーバー情報"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
|||||||
desktop/views/pages/deck/deck.tl-column.vue:
|
desktop/views/pages/deck/deck.tl-column.vue:
|
||||||
is-media-only: "メディア投稿のみ"
|
is-media-only: "メディア投稿のみ"
|
||||||
is-media-view: "メディアビュー"
|
is-media-view: "メディアビュー"
|
||||||
|
desktop/views/pages/deck/deck.note.vue:
|
||||||
|
reposted-by: "{}がRenote"
|
||||||
|
private: "この投稿は非公開です"
|
||||||
|
deleted: "この投稿は削除されました"
|
||||||
desktop/views/pages/welcome.vue:
|
desktop/views/pages/welcome.vue:
|
||||||
about: "詳しく..."
|
about: "詳しく..."
|
||||||
gotit: "わかった"
|
gotit: "わかった"
|
||||||
|
@ -70,6 +70,7 @@ common:
|
|||||||
donation: "寄付のお願い"
|
donation: "寄付のお願い"
|
||||||
nav: "ナビゲーション"
|
nav: "ナビゲーション"
|
||||||
tips: "ヒント"
|
tips: "ヒント"
|
||||||
|
hashtags: "ハッシュタグ"
|
||||||
deck:
|
deck:
|
||||||
widgets: "ウィジェット"
|
widgets: "ウィジェット"
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
@ -224,6 +225,9 @@ common/views/widgets/photo-stream.vue:
|
|||||||
common/views/widgets/posts-monitor.vue:
|
common/views/widgets/posts-monitor.vue:
|
||||||
title: "投稿チャート"
|
title: "投稿チャート"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
|
common/views/widgets/hashtags.vue:
|
||||||
|
title: "ハッシュタグ"
|
||||||
|
count: "{}人が投稿"
|
||||||
common/views/widgets/server.vue:
|
common/views/widgets/server.vue:
|
||||||
title: "サーバー情報"
|
title: "サーバー情報"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
@ -576,6 +580,10 @@ desktop/views/components/window.vue:
|
|||||||
desktop/views/pages/deck/deck.tl-column.vue:
|
desktop/views/pages/deck/deck.tl-column.vue:
|
||||||
is-media-only: "メディア投稿のみ"
|
is-media-only: "メディア投稿のみ"
|
||||||
is-media-view: "メディアビュー"
|
is-media-view: "メディアビュー"
|
||||||
|
desktop/views/pages/deck/deck.note.vue:
|
||||||
|
reposted-by: "{}がRenote"
|
||||||
|
private: "この投稿は非公開です"
|
||||||
|
deleted: "この投稿は削除されました"
|
||||||
desktop/views/pages/welcome.vue:
|
desktop/views/pages/welcome.vue:
|
||||||
about: "詳しく..."
|
about: "詳しく..."
|
||||||
gotit: "わかった"
|
gotit: "わかった"
|
||||||
|
124
package.json
124
package.json
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "2.35.1",
|
"version": "3.0.1",
|
||||||
"clientVersion": "1.0.6360",
|
"clientVersion": "1.0.6517",
|
||||||
"codename": "nighthike",
|
"codename": "nighthike",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
@ -29,6 +29,65 @@
|
|||||||
"@fortawesome/fontawesome-free-solid": "5.0.2",
|
"@fortawesome/fontawesome-free-solid": "5.0.2",
|
||||||
"@koa/cors": "2.2.1",
|
"@koa/cors": "2.2.1",
|
||||||
"@prezzemolo/rap": "0.1.2",
|
"@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",
|
||||||
|
|
||||||
"@prezzemolo/zip": "0.0.3",
|
"@prezzemolo/zip": "0.0.3",
|
||||||
"@types/bcryptjs": "2.4.1",
|
"@types/bcryptjs": "2.4.1",
|
||||||
"@types/debug": "0.0.30",
|
"@types/debug": "0.0.30",
|
||||||
@ -84,30 +143,17 @@
|
|||||||
"@types/ws": "5.1.1",
|
"@types/ws": "5.1.1",
|
||||||
"animejs": "2.2.0",
|
"animejs": "2.2.0",
|
||||||
"autosize": "4.0.2",
|
"autosize": "4.0.2",
|
||||||
"autwh": "0.1.0",
|
|
||||||
"bcryptjs": "2.4.3",
|
|
||||||
"bootstrap-vue": "2.0.0-rc.6",
|
"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",
|
"css-loader": "0.28.11",
|
||||||
"debug": "3.1.0",
|
|
||||||
"deep-equal": "1.0.1",
|
"deep-equal": "1.0.1",
|
||||||
"deepcopy": "0.6.3",
|
|
||||||
"diskusage": "0.2.4",
|
|
||||||
"dompurify": "1.0.4",
|
"dompurify": "1.0.4",
|
||||||
"elasticsearch": "15.0.0",
|
|
||||||
"element-ui": "2.3.9",
|
"element-ui": "2.3.9",
|
||||||
"emojilib": "2.2.12",
|
|
||||||
"escape-regexp": "0.0.1",
|
|
||||||
"eslint": "4.19.1",
|
"eslint": "4.19.1",
|
||||||
"eslint-plugin-vue": "4.5.0",
|
"eslint-plugin-vue": "4.5.0",
|
||||||
"eventemitter3": "3.1.0",
|
"eventemitter3": "3.1.0",
|
||||||
"exif-js": "2.3.0",
|
"exif-js": "2.3.0",
|
||||||
"file-loader": "1.1.11",
|
"file-loader": "1.1.11",
|
||||||
"file-type": "8.0.0",
|
|
||||||
"fuckadblock": "3.2.1",
|
"fuckadblock": "3.2.1",
|
||||||
"gm": "1.23.1",
|
|
||||||
"gulp": "3.9.1",
|
"gulp": "3.9.1",
|
||||||
"gulp-cssnano": "2.1.3",
|
"gulp-cssnano": "2.1.3",
|
||||||
"gulp-htmlmin": "4.0.0",
|
"gulp-htmlmin": "4.0.0",
|
||||||
@ -125,71 +171,32 @@
|
|||||||
"hard-source-webpack-plugin": "0.6.10",
|
"hard-source-webpack-plugin": "0.6.10",
|
||||||
"highlight.js": "9.12.0",
|
"highlight.js": "9.12.0",
|
||||||
"html-minifier": "3.5.16",
|
"html-minifier": "3.5.16",
|
||||||
"http-signature": "1.2.0",
|
|
||||||
"inquirer": "5.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",
|
"license-checker": "20.0.0",
|
||||||
"loader-utils": "1.1.0",
|
"loader-utils": "1.1.0",
|
||||||
"mecab-async": "0.1.2",
|
"mecab-async": "0.1.2",
|
||||||
"mkdirp": "0.5.1",
|
"mkdirp": "0.5.1",
|
||||||
"mocha": "5.2.0",
|
"mocha": "5.2.0",
|
||||||
"moji": "0.5.1",
|
"moji": "0.5.1",
|
||||||
"mongodb": "3.0.10",
|
|
||||||
"monk": "6.0.6",
|
|
||||||
"ms": "2.1.1",
|
|
||||||
"nan": "2.10.0",
|
"nan": "2.10.0",
|
||||||
"node-sass": "4.9.0",
|
"node-sass": "4.9.0",
|
||||||
"node-sass-json-importer": "3.2.0",
|
"node-sass-json-importer": "3.2.0",
|
||||||
"nopt": "4.0.1",
|
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"object-assign-deep": "0.4.0",
|
"object-assign-deep": "0.4.0",
|
||||||
"on-build-webpack": "0.1.0",
|
"on-build-webpack": "0.1.0",
|
||||||
"os-utils": "0.0.14",
|
|
||||||
"parse5": "5.0.0",
|
|
||||||
"progress-bar-webpack-plugin": "1.11.0",
|
"progress-bar-webpack-plugin": "1.11.0",
|
||||||
"prominence": "0.2.0",
|
|
||||||
"promise-sequential": "1.1.1",
|
|
||||||
"pug": "2.0.3",
|
"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",
|
"rimraf": "2.6.2",
|
||||||
"rndstr": "1.0.0",
|
|
||||||
"s-age": "1.1.2",
|
"s-age": "1.1.2",
|
||||||
"sass-loader": "7.0.1",
|
"sass-loader": "7.0.1",
|
||||||
"seedrandom": "2.4.3",
|
"seedrandom": "2.4.3",
|
||||||
"single-line-log": "1.1.2",
|
"single-line-log": "1.1.2",
|
||||||
"speakeasy": "2.0.0",
|
|
||||||
"style-loader": "0.21.0",
|
"style-loader": "0.21.0",
|
||||||
"stylus": "0.54.5",
|
"stylus": "0.54.5",
|
||||||
"stylus-loader": "3.0.2",
|
"stylus-loader": "3.0.2",
|
||||||
"summaly": "2.0.6",
|
|
||||||
"swagger-jsdoc": "1.9.7",
|
"swagger-jsdoc": "1.9.7",
|
||||||
"syuilo-password-strength": "0.0.1",
|
"syuilo-password-strength": "0.0.1",
|
||||||
"tcp-port-used": "0.1.2",
|
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"tmp": "0.0.33",
|
|
||||||
"ts-loader": "4.3.0",
|
"ts-loader": "4.3.0",
|
||||||
"ts-node": "6.0.4",
|
"ts-node": "6.0.4",
|
||||||
"tslint": "5.10.0",
|
"tslint": "5.10.0",
|
||||||
@ -197,25 +204,18 @@
|
|||||||
"typescript-eslint-parser": "15.0.0",
|
"typescript-eslint-parser": "15.0.0",
|
||||||
"uglify-es": "3.3.9",
|
"uglify-es": "3.3.9",
|
||||||
"url-loader": "1.0.1",
|
"url-loader": "1.0.1",
|
||||||
"uuid": "3.2.1",
|
|
||||||
"v-animate-css": "0.0.2",
|
"v-animate-css": "0.0.2",
|
||||||
"vue": "2.5.16",
|
"vue": "2.5.16",
|
||||||
"vue-cropperjs": "2.2.0",
|
"vue-cropperjs": "2.2.0",
|
||||||
"vue-js-modal": "1.3.13",
|
"vue-js-modal": "1.3.13",
|
||||||
"vue-json-tree-view": "2.1.4",
|
"vue-json-tree-view": "2.1.4",
|
||||||
"vue-loader": "15.2.1",
|
"vue-loader": "15.2.1",
|
||||||
"vue-material": "^1.0.0-beta-10.2",
|
|
||||||
"vue-router": "3.0.1",
|
"vue-router": "3.0.1",
|
||||||
"vue-template-compiler": "2.5.16",
|
"vue-template-compiler": "2.5.16",
|
||||||
"vuedraggable": "2.16.0",
|
"vuedraggable": "2.16.0",
|
||||||
"vuex": "3.0.1",
|
"vuex": "3.0.1",
|
||||||
"vuex-persistedstate": "^2.5.4",
|
"vuex-persistedstate": "^2.5.4",
|
||||||
"web-push": "3.3.1",
|
|
||||||
"webfinger.js": "2.6.6",
|
|
||||||
"webpack": "4.9.1",
|
"webpack": "4.9.1",
|
||||||
"webpack-cli": "2.1.4",
|
"webpack-cli": "2.1.4"
|
||||||
"websocket": "1.0.26",
|
|
||||||
"ws": "5.2.0",
|
|
||||||
"xev": "2.0.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,6 @@ html
|
|||||||
cursor progress !important
|
cursor progress !important
|
||||||
|
|
||||||
body
|
body
|
||||||
// for md
|
|
||||||
font-size 16px !important
|
|
||||||
line-height initial !important
|
|
||||||
letter-spacing initial !important
|
|
||||||
|
|
||||||
overflow-wrap break-word
|
overflow-wrap break-word
|
||||||
|
|
||||||
#error
|
#error
|
||||||
|
@ -13,9 +13,6 @@
|
|||||||
|
|
||||||
.a
|
.a
|
||||||
display block
|
display block
|
||||||
position fixed
|
|
||||||
top 0
|
|
||||||
right 0
|
|
||||||
|
|
||||||
> svg
|
> svg
|
||||||
display block
|
display block
|
||||||
|
@ -29,6 +29,14 @@ import fileTypeIcon from './file-type-icon.vue';
|
|||||||
import Switch from './switch.vue';
|
import Switch from './switch.vue';
|
||||||
import Othello from './othello.vue';
|
import Othello from './othello.vue';
|
||||||
import welcomeTimeline from './welcome-timeline.vue';
|
import welcomeTimeline from './welcome-timeline.vue';
|
||||||
|
import uiInput from './ui/input.vue';
|
||||||
|
import uiButton from './ui/button.vue';
|
||||||
|
import uiCard from './ui/card.vue';
|
||||||
|
import uiForm from './ui/form.vue';
|
||||||
|
import uiTextarea from './ui/textarea.vue';
|
||||||
|
import uiSwitch from './ui/switch.vue';
|
||||||
|
import uiRadio from './ui/radio.vue';
|
||||||
|
import uiSelect from './ui/select.vue';
|
||||||
|
|
||||||
Vue.component('mk-analog-clock', analogClock);
|
Vue.component('mk-analog-clock', analogClock);
|
||||||
Vue.component('mk-menu', menu);
|
Vue.component('mk-menu', menu);
|
||||||
@ -59,3 +67,11 @@ Vue.component('mk-file-type-icon', fileTypeIcon);
|
|||||||
Vue.component('mk-switch', Switch);
|
Vue.component('mk-switch', Switch);
|
||||||
Vue.component('mk-othello', Othello);
|
Vue.component('mk-othello', Othello);
|
||||||
Vue.component('mk-welcome-timeline', welcomeTimeline);
|
Vue.component('mk-welcome-timeline', welcomeTimeline);
|
||||||
|
Vue.component('ui-input', uiInput);
|
||||||
|
Vue.component('ui-button', uiButton);
|
||||||
|
Vue.component('ui-card', uiCard);
|
||||||
|
Vue.component('ui-form', uiForm);
|
||||||
|
Vue.component('ui-textarea', uiTextarea);
|
||||||
|
Vue.component('ui-switch', uiSwitch);
|
||||||
|
Vue.component('ui-radio', uiRadio);
|
||||||
|
Vue.component('ui-select', uiSelect);
|
||||||
|
@ -40,6 +40,17 @@ export default Vue.component('mk-note-html', {
|
|||||||
ast = this.ast;
|
ast = this.ast;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ast.filter(x => x.type != 'hashtag').length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ast[ast.length - 1] && (
|
||||||
|
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
|
// Parse ast to DOM
|
||||||
const els = flatten(ast.map(token => {
|
const els = flatten(ast.map(token => {
|
||||||
switch (token.type) {
|
switch (token.type) {
|
||||||
@ -92,7 +103,7 @@ export default Vue.component('mk-note-html', {
|
|||||||
case 'hashtag':
|
case 'hashtag':
|
||||||
return createElement('a', {
|
return createElement('a', {
|
||||||
attrs: {
|
attrs: {
|
||||||
href: `${url}/search?q=${token.content}`,
|
href: `${url}/tags/${token.hashtag}`,
|
||||||
target: '_blank'
|
target: '_blank'
|
||||||
}
|
}
|
||||||
}, token.content);
|
}, token.content);
|
||||||
|
@ -1,24 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit">
|
<form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit">
|
||||||
<label class="user-name">
|
<div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
|
||||||
<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="%i18n:@username%" autofocus required @change="onUsernameChange"/>%fa:at%
|
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange">
|
||||||
</label>
|
<span>%i18n:@username%</span>
|
||||||
<label class="password">
|
<span slot="prefix">@</span>
|
||||||
<input v-model="password" type="password" placeholder="%i18n:@password%" required/>%fa:lock%
|
<span slot="suffix">@{{ host }}</span>
|
||||||
</label>
|
</ui-input>
|
||||||
<label class="token" v-if="user && user.twoFactorEnabled">
|
<ui-input v-model="password" type="password" required>
|
||||||
<input v-model="token" type="number" placeholder="%i18n:@token%" required/>%fa:lock%
|
<span>%i18n:@password%</span>
|
||||||
</label>
|
<span slot="prefix">%fa:lock%</span>
|
||||||
<button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</button>
|
</ui-input>
|
||||||
もしくは <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
|
<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required/>
|
||||||
|
<ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
|
||||||
|
<p style="margin: 8px 0;">または<a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a></p>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { apiUrl } from '../../../config';
|
import { apiUrl, host } from '../../../config';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
withAvatar: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
signing: false,
|
signing: false,
|
||||||
@ -27,6 +36,7 @@ export default Vue.extend({
|
|||||||
password: '',
|
password: '',
|
||||||
token: '',
|
token: '',
|
||||||
apiUrl,
|
apiUrl,
|
||||||
|
host
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -35,6 +45,8 @@ export default Vue.extend({
|
|||||||
username: this.username
|
username: this.username
|
||||||
}).then(user => {
|
}).then(user => {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
}, () => {
|
||||||
|
this.user = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
@ -59,84 +71,19 @@ export default Vue.extend({
|
|||||||
@import '~const.styl'
|
@import '~const.styl'
|
||||||
|
|
||||||
.mk-signin
|
.mk-signin
|
||||||
|
color #555
|
||||||
|
|
||||||
&.signing
|
&.signing
|
||||||
&, *
|
&, *
|
||||||
cursor wait !important
|
cursor wait !important
|
||||||
|
|
||||||
label
|
> .avatar
|
||||||
display block
|
margin 16px auto 0 auto
|
||||||
margin 12px 0
|
width 64px
|
||||||
|
height 64px
|
||||||
[data-fa]
|
background #ddd
|
||||||
display block
|
background-position center
|
||||||
pointer-events none
|
background-size cover
|
||||||
position absolute
|
border-radius 100%
|
||||||
bottom 0
|
|
||||||
top 0
|
|
||||||
left 0
|
|
||||||
z-index 1
|
|
||||||
margin auto
|
|
||||||
padding 0 16px
|
|
||||||
height 1em
|
|
||||||
color #898786
|
|
||||||
|
|
||||||
input[type=text]
|
|
||||||
input[type=password]
|
|
||||||
input[type=number]
|
|
||||||
user-select text
|
|
||||||
display inline-block
|
|
||||||
cursor auto
|
|
||||||
padding 0 0 0 38px
|
|
||||||
margin 0
|
|
||||||
width 100%
|
|
||||||
line-height 44px
|
|
||||||
font-size 1em
|
|
||||||
color rgba(#000, 0.7)
|
|
||||||
background #fff
|
|
||||||
outline none
|
|
||||||
border solid 1px #eee
|
|
||||||
border-radius 4px
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
background rgba(255, 255, 255, 0.7)
|
|
||||||
border-color #ddd
|
|
||||||
|
|
||||||
& + i
|
|
||||||
color #797776
|
|
||||||
|
|
||||||
&:focus
|
|
||||||
background #fff
|
|
||||||
border-color #ccc
|
|
||||||
|
|
||||||
& + i
|
|
||||||
color #797776
|
|
||||||
|
|
||||||
[type=submit]
|
|
||||||
cursor pointer
|
|
||||||
padding 16px
|
|
||||||
margin -6px 0 0 0
|
|
||||||
width 100%
|
|
||||||
font-size 1.2em
|
|
||||||
color rgba(#000, 0.5)
|
|
||||||
outline none
|
|
||||||
border none
|
|
||||||
border-radius 0
|
|
||||||
background transparent
|
|
||||||
transition all .5s ease
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
color $theme-color
|
|
||||||
transition all .2s ease
|
|
||||||
|
|
||||||
&:focus
|
|
||||||
color $theme-color
|
|
||||||
transition all .2s ease
|
|
||||||
|
|
||||||
&:active
|
|
||||||
color darken($theme-color, 30%)
|
|
||||||
transition all .2s ease
|
|
||||||
|
|
||||||
&:disabled
|
|
||||||
opacity 0.7
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,60 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<form class="mk-signup" @submit.prevent="onSubmit" autocomplete="off">
|
<form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
|
||||||
<label class="username">
|
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername">
|
||||||
<p class="caption">%fa:at%%i18n:@username%</p>
|
<span>%i18n:@username%</span>
|
||||||
<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @input="onChangeUsername"/>
|
<span slot="prefix">@</span>
|
||||||
<p class="profile-page-url-preview" v-if="shouldShowProfileUrl">{{ `${url}/@${username}` }}</p>
|
<span slot="suffix">@{{ host }}</span>
|
||||||
<p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:@checking%</p>
|
<p slot="text" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw% %i18n:@checking%</p>
|
||||||
<p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:@available%</p>
|
<p slot="text" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw% %i18n:@available%</p>
|
||||||
<p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@unavailable%</p>
|
<p slot="text" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@unavailable%</p>
|
||||||
<p class="info" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@error%</p>
|
<p slot="text" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@error%</p>
|
||||||
<p class="info" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@invalid-format%</p>
|
<p slot="text" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@invalid-format%</p>
|
||||||
<p class="info" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-short%</p>
|
<p slot="text" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-short%</p>
|
||||||
<p class="info" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-long%</p>
|
<p slot="text" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-long%</p>
|
||||||
</label>
|
</ui-input>
|
||||||
<label class="password">
|
<ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true">
|
||||||
<p class="caption">%fa:lock%%i18n:@password%</p>
|
<span>%i18n:@password%</span>
|
||||||
<input v-model="password" type="password" placeholder="%i18n:@password-placeholder%" autocomplete="off" required @input="onChangePassword"/>
|
<span slot="prefix">%fa:lock%</span>
|
||||||
<div class="meter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
|
<div slot="text">
|
||||||
<div class="value" ref="passwordMetar"></div>
|
<p slot="text" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@weak-password%</p>
|
||||||
|
<p slot="text" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw% %i18n:@normal-password%</p>
|
||||||
|
<p slot="text" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw% %i18n:@strong-password%</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@weak-password%</p>
|
</ui-input>
|
||||||
<p class="info" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw%%i18n:@normal-password%</p>
|
<ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype">
|
||||||
<p class="info" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw%%i18n:@strong-password%</p>
|
<span>%i18n:@password% (%i18n:@retype%)</span>
|
||||||
</label>
|
<span slot="prefix">%fa:lock%</span>
|
||||||
<label class="retype-password">
|
<div slot="text">
|
||||||
<p class="caption">%fa:lock%%i18n:@password%(%i18n:@retype%)</p>
|
<p slot="text" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw% %i18n:@password-matched%</p>
|
||||||
<input v-model="retypedPassword" type="password" placeholder="%i18n:@retype-placeholder%" autocomplete="off" required @input="onChangePasswordRetype"/>
|
<p slot="text" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@password-not-matched%</p>
|
||||||
<p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:@password-matched%</p>
|
</div>
|
||||||
<p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@password-not-matched%</p>
|
</ui-input>
|
||||||
</label>
|
<div class="g-recaptcha" :data-sitekey="recaptchaSitekey" style="margin: 16px 0;"></div>
|
||||||
<label class="recaptcha">
|
<label class="agree-tou" style="display: block; margin: 16px 0;">
|
||||||
<p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:@recaptcha%</p>
|
<input name="agree-tou" type="checkbox" required/>
|
||||||
<div class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" :data-sitekey="recaptchaSitekey"></div>
|
|
||||||
</label>
|
|
||||||
<label class="agree-tou">
|
|
||||||
<input name="agree-tou" type="checkbox" autocomplete="off" required/>
|
|
||||||
<p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p>
|
<p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit">%i18n:@create%</button>
|
<ui-button type="submit">%i18n:@create%</ui-button>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
const getPasswordStrength = require('syuilo-password-strength');
|
const getPasswordStrength = require('syuilo-password-strength');
|
||||||
import { url, docsUrl, lang, recaptchaSitekey } from '../../../config';
|
import { host, url, docsUrl, lang, recaptchaSitekey } from '../../../config';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
host,
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
retypedPassword: '',
|
retypedPassword: '',
|
||||||
url,
|
url,
|
||||||
touUrl: `${docsUrl}/${lang}/tou`,
|
touUrl: `${docsUrl}/${lang}/tou`,
|
||||||
recaptchaSitekey,
|
recaptchaSitekey,
|
||||||
recaptchaed: false,
|
|
||||||
usernameState: null,
|
usernameState: null,
|
||||||
passwordStrength: '',
|
passwordStrength: '',
|
||||||
passwordRetypeState: null
|
passwordRetypeState: null
|
||||||
@ -104,7 +102,6 @@ export default Vue.extend({
|
|||||||
|
|
||||||
const strength = getPasswordStrength(this.password);
|
const strength = getPasswordStrength(this.password);
|
||||||
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||||
(this.$refs.passwordMetar as any).style.width = `${strength * 100}%`;
|
|
||||||
},
|
},
|
||||||
onChangePasswordRetype() {
|
onChangePasswordRetype() {
|
||||||
if (this.retypedPassword == '') {
|
if (this.retypedPassword == '') {
|
||||||
@ -130,19 +127,9 @@ export default Vue.extend({
|
|||||||
alert('%i18n:@some-error%');
|
alert('%i18n:@some-error%');
|
||||||
|
|
||||||
(window as any).grecaptcha.reset();
|
(window as any).grecaptcha.reset();
|
||||||
this.recaptchaed = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
(window as any).onRecaptchaed = () => {
|
|
||||||
this.recaptchaed = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
(window as any).onRecaptchaExpired = () => {
|
|
||||||
this.recaptchaed = false;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
mounted() {
|
||||||
const head = document.getElementsByTagName('head')[0];
|
const head = document.getElementsByTagName('head')[0];
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
@ -158,100 +145,6 @@ export default Vue.extend({
|
|||||||
.mk-signup
|
.mk-signup
|
||||||
min-width 302px
|
min-width 302px
|
||||||
|
|
||||||
label
|
|
||||||
display block
|
|
||||||
margin 0 0 16px 0
|
|
||||||
|
|
||||||
> .caption
|
|
||||||
margin 0 0 4px 0
|
|
||||||
color #828888
|
|
||||||
font-size 0.95em
|
|
||||||
|
|
||||||
> [data-fa]
|
|
||||||
margin-right 0.25em
|
|
||||||
color #96adac
|
|
||||||
|
|
||||||
> .info
|
|
||||||
display block
|
|
||||||
margin 4px 0
|
|
||||||
font-size 0.8em
|
|
||||||
|
|
||||||
> [data-fa]
|
|
||||||
margin-right 0.3em
|
|
||||||
|
|
||||||
&.username
|
|
||||||
.profile-page-url-preview
|
|
||||||
display block
|
|
||||||
margin 4px 8px 0 4px
|
|
||||||
font-size 0.8em
|
|
||||||
color #888
|
|
||||||
|
|
||||||
&:empty
|
|
||||||
display none
|
|
||||||
|
|
||||||
&:not(:empty) + .info
|
|
||||||
margin-top 0
|
|
||||||
|
|
||||||
&.password
|
|
||||||
.meter
|
|
||||||
display block
|
|
||||||
margin-top 8px
|
|
||||||
width 100%
|
|
||||||
height 8px
|
|
||||||
|
|
||||||
&[data-strength='']
|
|
||||||
display none
|
|
||||||
|
|
||||||
&[data-strength='low']
|
|
||||||
> .value
|
|
||||||
background #d73612
|
|
||||||
|
|
||||||
&[data-strength='medium']
|
|
||||||
> .value
|
|
||||||
background #d7ca12
|
|
||||||
|
|
||||||
&[data-strength='high']
|
|
||||||
> .value
|
|
||||||
background #61bb22
|
|
||||||
|
|
||||||
> .value
|
|
||||||
display block
|
|
||||||
width 0%
|
|
||||||
height 100%
|
|
||||||
background transparent
|
|
||||||
border-radius 4px
|
|
||||||
transition all 0.1s ease
|
|
||||||
|
|
||||||
[type=text], [type=password]
|
|
||||||
user-select text
|
|
||||||
display inline-block
|
|
||||||
cursor auto
|
|
||||||
padding 0 12px
|
|
||||||
margin 0
|
|
||||||
width 100%
|
|
||||||
line-height 44px
|
|
||||||
font-size 1em
|
|
||||||
color #333 !important
|
|
||||||
background #fff !important
|
|
||||||
outline none
|
|
||||||
border solid 1px rgba(#000, 0.1)
|
|
||||||
border-radius 4px
|
|
||||||
box-shadow 0 0 0 114514px #fff inset
|
|
||||||
transition all .3s ease
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
border-color rgba(#000, 0.2)
|
|
||||||
transition all .1s ease
|
|
||||||
|
|
||||||
&:focus
|
|
||||||
color $theme-color !important
|
|
||||||
border-color $theme-color
|
|
||||||
box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%)
|
|
||||||
transition all 0s ease
|
|
||||||
|
|
||||||
&:disabled
|
|
||||||
opacity 0.5
|
|
||||||
|
|
||||||
.agree-tou
|
.agree-tou
|
||||||
padding 4px
|
padding 4px
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
@ -269,19 +162,4 @@ export default Vue.extend({
|
|||||||
display inline
|
display inline
|
||||||
color #555
|
color #555
|
||||||
|
|
||||||
button
|
|
||||||
margin 0
|
|
||||||
padding 16px
|
|
||||||
width 100%
|
|
||||||
font-size 1em
|
|
||||||
color #fff
|
|
||||||
background $theme-color
|
|
||||||
border-radius 3px
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
background lighten($theme-color, 5%)
|
|
||||||
|
|
||||||
&:active
|
|
||||||
background darken($theme-color, 5%)
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -58,18 +58,21 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.mode == 'relative' || this.mode == 'detail') {
|
if (this.mode == 'relative' || this.mode == 'detail') {
|
||||||
this.tick();
|
this.tickId = window.requestAnimationFrame(this.tick);
|
||||||
this.tickId = setInterval(this.tick, 10000);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
if (this.mode === 'relative' || this.mode === 'detail') {
|
if (this.mode === 'relative' || this.mode === 'detail') {
|
||||||
clearInterval(this.tickId);
|
window.clearTimeout(this.tickId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
tick() {
|
tick() {
|
||||||
this.now = new Date();
|
this.now = new Date();
|
||||||
|
|
||||||
|
this.tickId = setTimeout(() => {
|
||||||
|
window.requestAnimationFrame(this.tick);
|
||||||
|
}, 10000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
82
src/client/app/common/views/components/ui/button.vue
Normal file
82
src/client/app/common/views/components/ui/button.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ui-button" :class="[styl]">
|
||||||
|
<button :type="type" @click="$emit('click')">
|
||||||
|
<slot></slot>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
styl: 'fill'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
isCardChild: { default: false }
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.isCardChild) {
|
||||||
|
this.styl = 'line';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark, fill)
|
||||||
|
> button
|
||||||
|
display block
|
||||||
|
width 100%
|
||||||
|
margin 0
|
||||||
|
padding 0
|
||||||
|
font-weight bold
|
||||||
|
font-size 16px
|
||||||
|
line-height 44px
|
||||||
|
border none
|
||||||
|
border-radius 6px
|
||||||
|
outline none
|
||||||
|
box-shadow none
|
||||||
|
|
||||||
|
if fill
|
||||||
|
color $theme-color-foreground
|
||||||
|
background $theme-color
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background lighten($theme-color, 5%)
|
||||||
|
|
||||||
|
&:active
|
||||||
|
background darken($theme-color, 5%)
|
||||||
|
else
|
||||||
|
color $theme-color
|
||||||
|
background none
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
color darken($theme-color, 5%)
|
||||||
|
|
||||||
|
&:active
|
||||||
|
background rgba($theme-color, 0.3)
|
||||||
|
|
||||||
|
.ui-button[data-darkmode]
|
||||||
|
&.fill
|
||||||
|
root(true, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(true, false)
|
||||||
|
|
||||||
|
.ui-button:not([data-darkmode])
|
||||||
|
&.fill
|
||||||
|
root(false, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(false, false)
|
||||||
|
|
||||||
|
</style>
|
46
src/client/app/common/views/components/ui/card.vue
Normal file
46
src/client/app/common/views/components/ui/card.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ui-card">
|
||||||
|
<header>
|
||||||
|
<slot name="title"></slot>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue.extend({
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
isCardChild: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark)
|
||||||
|
margin 16px
|
||||||
|
padding 16px
|
||||||
|
color isDark ? #fff : #000
|
||||||
|
background isDark ? #282C37 : #fff
|
||||||
|
box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12)
|
||||||
|
|
||||||
|
@media (min-width 500px)
|
||||||
|
padding 32px
|
||||||
|
|
||||||
|
> header
|
||||||
|
font-weight normal
|
||||||
|
font-size 24px
|
||||||
|
color isDark ? #fff : #444
|
||||||
|
|
||||||
|
.ui-card[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.ui-card:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
|
</style>
|
30
src/client/app/common/views/components/ui/form.vue
Normal file
30
src/client/app/common/views/components/ui/form.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ui-form">
|
||||||
|
<fieldset :disabled="disabled">
|
||||||
|
<slot></slot>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
.ui-form
|
||||||
|
> fieldset
|
||||||
|
margin 0
|
||||||
|
padding 0
|
||||||
|
border none
|
||||||
|
|
||||||
|
</style>
|
346
src/client/app/common/views/components/ui/input.vue
Normal file
346
src/client/app/common/views/components/ui/input.vue
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ui-input" :class="[{ focused, filled }, styl]">
|
||||||
|
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
||||||
|
<div class="input">
|
||||||
|
<div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
|
||||||
|
<div class="value" ref="passwordMetar"></div>
|
||||||
|
</div>
|
||||||
|
<span class="label" ref="label"><slot></slot></span>
|
||||||
|
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
|
||||||
|
<template v-if="type != 'file'">
|
||||||
|
<input ref="input"
|
||||||
|
:type="type"
|
||||||
|
v-model="v"
|
||||||
|
:required="required"
|
||||||
|
:readonly="readonly"
|
||||||
|
:pattern="pattern"
|
||||||
|
:autocomplete="autocomplete"
|
||||||
|
:spellcheck="spellcheck"
|
||||||
|
@focus="focused = true"
|
||||||
|
@blur="focused = false">
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<input ref="input"
|
||||||
|
type="text"
|
||||||
|
:value="placeholder"
|
||||||
|
readonly
|
||||||
|
@click="chooseFile">
|
||||||
|
<input ref="file"
|
||||||
|
type="file"
|
||||||
|
:value="value"
|
||||||
|
@change="onChangeFile">
|
||||||
|
</template>
|
||||||
|
<div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
|
||||||
|
</div>
|
||||||
|
<div class="text"><slot name="text"></slot></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
const getPasswordStrength = require('syuilo-password-strength');
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
pattern: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
autocomplete: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
spellcheck: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
withPasswordMeter: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
v: this.value,
|
||||||
|
focused: false,
|
||||||
|
passwordStrength: '',
|
||||||
|
styl: 'fill'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filled(): boolean {
|
||||||
|
return this.v != '' && this.v != null;
|
||||||
|
},
|
||||||
|
placeholder(): string {
|
||||||
|
if (this.type != 'file') return null;
|
||||||
|
if (this.v == null) return null;
|
||||||
|
|
||||||
|
if (typeof this.v == 'string') return this.v;
|
||||||
|
|
||||||
|
if (Array.isArray(this.v)) {
|
||||||
|
return this.v.map(file => file.name).join(', ');
|
||||||
|
} else {
|
||||||
|
return this.v.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(v) {
|
||||||
|
this.v = v;
|
||||||
|
},
|
||||||
|
v(v) {
|
||||||
|
this.$emit('input', v);
|
||||||
|
|
||||||
|
if (this.withPasswordMeter) {
|
||||||
|
if (v == '') {
|
||||||
|
this.passwordStrength = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strength = getPasswordStrength(v);
|
||||||
|
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||||
|
(this.$refs.passwordMetar as any).style.width = `${strength * 100}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
isCardChild: { default: false }
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.isCardChild) {
|
||||||
|
this.styl = 'line';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.$refs.prefix) {
|
||||||
|
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
|
||||||
|
if (this.$refs.prefix.offsetWidth) {
|
||||||
|
this.$refs.input.style.paddingLeft = this.$refs.prefix.offsetWidth + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.$refs.suffix) {
|
||||||
|
if (this.$refs.suffix.offsetWidth) {
|
||||||
|
this.$refs.input.style.paddingRight = this.$refs.suffix.offsetWidth + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$refs.input.focus();
|
||||||
|
},
|
||||||
|
chooseFile() {
|
||||||
|
this.$refs.file.click();
|
||||||
|
},
|
||||||
|
onChangeFile() {
|
||||||
|
this.v = Array.from((this.$refs.file as any).files);
|
||||||
|
this.$emit('input', this.v);
|
||||||
|
this.$emit('change', this.v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark, fill)
|
||||||
|
margin 32px 0
|
||||||
|
|
||||||
|
> .icon
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
width 24px
|
||||||
|
text-align center
|
||||||
|
line-height 32px
|
||||||
|
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
|
||||||
|
|
||||||
|
&:not(:empty) + .input
|
||||||
|
margin-left 28px
|
||||||
|
|
||||||
|
> .input
|
||||||
|
|
||||||
|
if !fill
|
||||||
|
&:before
|
||||||
|
content ''
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
height 1px
|
||||||
|
background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content ''
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
height 2px
|
||||||
|
background $theme-color
|
||||||
|
opacity 0
|
||||||
|
transform scaleX(0.12)
|
||||||
|
transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
will-change border opacity transform
|
||||||
|
|
||||||
|
> .password-meter
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
border-radius 6px
|
||||||
|
overflow hidden
|
||||||
|
opacity 0.3
|
||||||
|
|
||||||
|
&[data-strength='']
|
||||||
|
display none
|
||||||
|
|
||||||
|
&[data-strength='low']
|
||||||
|
> .value
|
||||||
|
background #d73612
|
||||||
|
|
||||||
|
&[data-strength='medium']
|
||||||
|
> .value
|
||||||
|
background #d7ca12
|
||||||
|
|
||||||
|
&[data-strength='high']
|
||||||
|
> .value
|
||||||
|
background #61bb22
|
||||||
|
|
||||||
|
> .value
|
||||||
|
display block
|
||||||
|
width 0%
|
||||||
|
height 100%
|
||||||
|
background transparent
|
||||||
|
border-radius 6px
|
||||||
|
transition all 0.1s ease
|
||||||
|
|
||||||
|
> .label
|
||||||
|
position absolute
|
||||||
|
z-index 1
|
||||||
|
top fill ? 6px : 0
|
||||||
|
left 0
|
||||||
|
pointer-events none
|
||||||
|
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
|
||||||
|
transition-duration 0.3s
|
||||||
|
font-size 16px
|
||||||
|
line-height 32px
|
||||||
|
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
|
||||||
|
pointer-events none
|
||||||
|
//will-change transform
|
||||||
|
transform-origin top left
|
||||||
|
transform scale(1)
|
||||||
|
|
||||||
|
> input
|
||||||
|
display block
|
||||||
|
width 100%
|
||||||
|
margin 0
|
||||||
|
padding 0
|
||||||
|
font inherit
|
||||||
|
font-weight fill ? bold : normal
|
||||||
|
font-size 16px
|
||||||
|
line-height 32px
|
||||||
|
color isDark ? #fff : #000
|
||||||
|
background transparent
|
||||||
|
border none
|
||||||
|
border-radius 0
|
||||||
|
outline none
|
||||||
|
box-shadow none
|
||||||
|
|
||||||
|
if fill
|
||||||
|
padding 6px 12px
|
||||||
|
background rgba(#000, 0.035)
|
||||||
|
border-radius 6px
|
||||||
|
|
||||||
|
&[type='file']
|
||||||
|
display none
|
||||||
|
|
||||||
|
> .prefix
|
||||||
|
> .suffix
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
z-index 1
|
||||||
|
top 0
|
||||||
|
font-size 16px
|
||||||
|
line-height fill ? 44px : 32px
|
||||||
|
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
|
||||||
|
pointer-events none
|
||||||
|
|
||||||
|
&:empty
|
||||||
|
display none
|
||||||
|
|
||||||
|
> *
|
||||||
|
display block
|
||||||
|
min-width 16px
|
||||||
|
|
||||||
|
> .prefix
|
||||||
|
left 0
|
||||||
|
padding-right 4px
|
||||||
|
|
||||||
|
if fill
|
||||||
|
padding-left 12px
|
||||||
|
|
||||||
|
> .suffix
|
||||||
|
right 0
|
||||||
|
padding-left 4px
|
||||||
|
|
||||||
|
if fill
|
||||||
|
padding-right 12px
|
||||||
|
|
||||||
|
> .text
|
||||||
|
margin 6px 0
|
||||||
|
font-size 13px
|
||||||
|
|
||||||
|
*
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
&.focused
|
||||||
|
> .input
|
||||||
|
if fill
|
||||||
|
background rgba(#000, 0.05)
|
||||||
|
else
|
||||||
|
&:after
|
||||||
|
opacity 1
|
||||||
|
transform scaleX(1)
|
||||||
|
|
||||||
|
> .label
|
||||||
|
color $theme-color
|
||||||
|
|
||||||
|
&.focused
|
||||||
|
&.filled
|
||||||
|
> .input
|
||||||
|
> .label
|
||||||
|
top fill ? -24px : -17px
|
||||||
|
left 0 !important
|
||||||
|
transform scale(0.75)
|
||||||
|
|
||||||
|
.ui-input[data-darkmode]
|
||||||
|
&.fill
|
||||||
|
root(true, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(true, false)
|
||||||
|
|
||||||
|
.ui-input:not([data-darkmode])
|
||||||
|
&.fill
|
||||||
|
root(false, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(false, false)
|
||||||
|
|
||||||
|
</style>
|
120
src/client/app/common/views/components/ui/radio.vue
Normal file
120
src/client/app/common/views/components/ui/radio.vue
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="ui-radio"
|
||||||
|
:class="{ disabled, checked }"
|
||||||
|
:aria-checked="checked"
|
||||||
|
:aria-disabled="disabled"
|
||||||
|
@click="toggle"
|
||||||
|
>
|
||||||
|
<input type="radio"
|
||||||
|
:disabled="disabled"
|
||||||
|
>
|
||||||
|
<span class="button">
|
||||||
|
<span></span>
|
||||||
|
</span>
|
||||||
|
<span class="label"><slot></slot></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue.extend({
|
||||||
|
model: {
|
||||||
|
prop: 'model',
|
||||||
|
event: 'change'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
model: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
checked(): boolean {
|
||||||
|
return this.model === this.value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggle() {
|
||||||
|
this.$emit('change', this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark)
|
||||||
|
display inline-block
|
||||||
|
margin 32px 32px 32px 0
|
||||||
|
cursor pointer
|
||||||
|
transition all 0.3s
|
||||||
|
|
||||||
|
> *
|
||||||
|
user-select none
|
||||||
|
|
||||||
|
&.disabled
|
||||||
|
opacity 0.6
|
||||||
|
cursor not-allowed
|
||||||
|
|
||||||
|
&.checked
|
||||||
|
> .button
|
||||||
|
border-color $theme-color
|
||||||
|
|
||||||
|
&:after
|
||||||
|
background-color $theme-color
|
||||||
|
transform scale(1)
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
> input
|
||||||
|
position absolute
|
||||||
|
width 0
|
||||||
|
height 0
|
||||||
|
opacity 0
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
> .button
|
||||||
|
position absolute
|
||||||
|
width 20px
|
||||||
|
height 20px
|
||||||
|
background none
|
||||||
|
border solid 2px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
|
||||||
|
border-radius 100%
|
||||||
|
transition inherit
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content ''
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
top 3px
|
||||||
|
right 3px
|
||||||
|
bottom 3px
|
||||||
|
left 3px
|
||||||
|
border-radius 100%
|
||||||
|
opacity 0
|
||||||
|
transform scale(0)
|
||||||
|
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
|
||||||
|
|
||||||
|
> .label
|
||||||
|
margin-left 28px
|
||||||
|
display block
|
||||||
|
font-size 16px
|
||||||
|
line-height 20px
|
||||||
|
cursor pointer
|
||||||
|
|
||||||
|
.ui-radio[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.ui-radio:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
|
</style>
|
215
src/client/app/common/views/components/ui/select.vue
Normal file
215
src/client/app/common/views/components/ui/select.vue
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ui-select" :class="[{ focused, filled }, styl]">
|
||||||
|
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
||||||
|
<div class="input" @click="focus">
|
||||||
|
<span class="label" ref="label"><slot name="label"></slot></span>
|
||||||
|
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
|
||||||
|
<select ref="input"
|
||||||
|
:value="v"
|
||||||
|
:required="required"
|
||||||
|
@input="$emit('input', $event.target.value)"
|
||||||
|
@focus="focused = true"
|
||||||
|
@blur="focused = false">
|
||||||
|
<slot></slot>
|
||||||
|
</select>
|
||||||
|
<div class="suffix"><slot name="suffix"></slot></div>
|
||||||
|
</div>
|
||||||
|
<div class="text"><slot name="text"></slot></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
v: this.value,
|
||||||
|
focused: false,
|
||||||
|
styl: 'fill'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filled(): boolean {
|
||||||
|
return this.v != '' && this.v != null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(v) {
|
||||||
|
this.v = v;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
isCardChild: { default: false }
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.isCardChild) {
|
||||||
|
this.styl = 'line';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.$refs.prefix) {
|
||||||
|
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$refs.input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark, fill)
|
||||||
|
margin 32px 0
|
||||||
|
|
||||||
|
> .icon
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
width 24px
|
||||||
|
text-align center
|
||||||
|
line-height 32px
|
||||||
|
color rgba(#000, 0.54)
|
||||||
|
|
||||||
|
&:not(:empty) + .input
|
||||||
|
margin-left 28px
|
||||||
|
|
||||||
|
> .input
|
||||||
|
display flex
|
||||||
|
|
||||||
|
if fill
|
||||||
|
padding 6px 12px
|
||||||
|
background rgba(#000, 0.035)
|
||||||
|
border-radius 6px
|
||||||
|
else
|
||||||
|
&:before
|
||||||
|
content ''
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
height 1px
|
||||||
|
background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content ''
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
height 2px
|
||||||
|
background $theme-color
|
||||||
|
opacity 0
|
||||||
|
transform scaleX(0.12)
|
||||||
|
transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
will-change border opacity transform
|
||||||
|
|
||||||
|
> .label
|
||||||
|
position absolute
|
||||||
|
top fill ? 6px : 0
|
||||||
|
left 0
|
||||||
|
pointer-events none
|
||||||
|
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
|
||||||
|
transition-duration 0.3s
|
||||||
|
font-size 16px
|
||||||
|
line-height 32px
|
||||||
|
color rgba(#000, 0.54)
|
||||||
|
pointer-events none
|
||||||
|
//will-change transform
|
||||||
|
transform-origin top left
|
||||||
|
transform scale(1)
|
||||||
|
|
||||||
|
> select
|
||||||
|
display block
|
||||||
|
flex 1
|
||||||
|
width 100%
|
||||||
|
padding 0
|
||||||
|
font inherit
|
||||||
|
font-weight fill ? bold : normal
|
||||||
|
font-size 16px
|
||||||
|
height 32px
|
||||||
|
color isDark ? #fff : #000
|
||||||
|
background transparent
|
||||||
|
border none
|
||||||
|
border-radius 0
|
||||||
|
outline none
|
||||||
|
box-shadow none
|
||||||
|
|
||||||
|
*
|
||||||
|
color #000
|
||||||
|
|
||||||
|
> .prefix
|
||||||
|
> .suffix
|
||||||
|
display block
|
||||||
|
align-self center
|
||||||
|
justify-self center
|
||||||
|
font-size 16px
|
||||||
|
line-height 32px
|
||||||
|
color rgba(#000, 0.54)
|
||||||
|
pointer-events none
|
||||||
|
|
||||||
|
> *
|
||||||
|
display block
|
||||||
|
min-width 16px
|
||||||
|
|
||||||
|
> .prefix
|
||||||
|
padding-right 4px
|
||||||
|
|
||||||
|
> .suffix
|
||||||
|
padding-left 4px
|
||||||
|
|
||||||
|
> .text
|
||||||
|
margin 6px 0
|
||||||
|
font-size 13px
|
||||||
|
|
||||||
|
*
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
&.focused
|
||||||
|
> .input
|
||||||
|
if fill
|
||||||
|
background rgba(#000, 0.05)
|
||||||
|
else
|
||||||
|
&:after
|
||||||
|
opacity 1
|
||||||
|
transform scaleX(1)
|
||||||
|
|
||||||
|
> .label
|
||||||
|
color $theme-color
|
||||||
|
|
||||||
|
&.focused
|
||||||
|
&.filled
|
||||||
|
> .input
|
||||||
|
> .label
|
||||||
|
top fill ? -24px : -17px
|
||||||
|
left 0 !important
|
||||||
|
transform scale(0.75)
|
||||||
|
|
||||||
|
.ui-select[data-darkmode]
|
||||||
|
&.fill
|
||||||
|
root(true, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(true, false)
|
||||||
|
|
||||||
|
.ui-select:not([data-darkmode])
|
||||||
|
&.fill
|
||||||
|
root(false, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(false, false)
|
||||||
|
|
||||||
|
</style>
|
135
src/client/app/common/views/components/ui/switch.vue
Normal file
135
src/client/app/common/views/components/ui/switch.vue
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="ui-switch"
|
||||||
|
:class="{ disabled, checked }"
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="checked"
|
||||||
|
:aria-disabled="disabled"
|
||||||
|
@click="toggle"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
ref="input"
|
||||||
|
:disabled="disabled"
|
||||||
|
@keydown.enter="toggle"
|
||||||
|
>
|
||||||
|
<span class="button">
|
||||||
|
<span></span>
|
||||||
|
</span>
|
||||||
|
<span class="label">
|
||||||
|
<span :aria-hidden="!checked"><slot></slot></span>
|
||||||
|
<p :aria-hidden="!checked">
|
||||||
|
<slot name="text"></slot>
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue.extend({
|
||||||
|
model: {
|
||||||
|
prop: 'value',
|
||||||
|
event: 'change'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
checked(): boolean {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggle() {
|
||||||
|
this.$emit('change', !this.checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark)
|
||||||
|
display flex
|
||||||
|
margin 32px 0
|
||||||
|
cursor pointer
|
||||||
|
transition all 0.3s
|
||||||
|
|
||||||
|
> *
|
||||||
|
user-select none
|
||||||
|
|
||||||
|
&.disabled
|
||||||
|
opacity 0.6
|
||||||
|
cursor not-allowed
|
||||||
|
|
||||||
|
&.checked
|
||||||
|
> .button
|
||||||
|
background-color rgba($theme-color, 0.4)
|
||||||
|
border-color rgba($theme-color, 0.4)
|
||||||
|
|
||||||
|
> *
|
||||||
|
background-color $theme-color
|
||||||
|
transform translateX(14px)
|
||||||
|
|
||||||
|
> input
|
||||||
|
position absolute
|
||||||
|
width 0
|
||||||
|
height 0
|
||||||
|
opacity 0
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
> .button
|
||||||
|
display inline-block
|
||||||
|
margin 3px 0 0 0
|
||||||
|
width 34px
|
||||||
|
height 14px
|
||||||
|
background isDark ? rgba(#fff, 0.15) : rgba(#000, 0.25)
|
||||||
|
outline none
|
||||||
|
border-radius 14px
|
||||||
|
transition inherit
|
||||||
|
|
||||||
|
> *
|
||||||
|
position absolute
|
||||||
|
top -3px
|
||||||
|
left 0
|
||||||
|
border-radius 100%
|
||||||
|
transition background-color 0.3s, transform 0.3s
|
||||||
|
width 20px
|
||||||
|
height 20px
|
||||||
|
background-color #fff
|
||||||
|
box-shadow 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12)
|
||||||
|
|
||||||
|
> .label
|
||||||
|
margin-left 8px
|
||||||
|
display block
|
||||||
|
font-size 16px
|
||||||
|
cursor pointer
|
||||||
|
transition inherit
|
||||||
|
|
||||||
|
> span
|
||||||
|
display block
|
||||||
|
line-height 20px
|
||||||
|
color isDark ? #c4ccd2 : rgba(#000, 0.75)
|
||||||
|
transition inherit
|
||||||
|
|
||||||
|
> p
|
||||||
|
margin 0
|
||||||
|
//font-size 90%
|
||||||
|
color isDark ? #78858e : #9daab3
|
||||||
|
|
||||||
|
.ui-switch[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.ui-switch:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
|
</style>
|
174
src/client/app/common/views/components/ui/textarea.vue
Normal file
174
src/client/app/common/views/components/ui/textarea.vue
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ui-textarea" :class="{ focused, filled }">
|
||||||
|
<div class="input">
|
||||||
|
<span class="label" ref="label"><slot></slot></span>
|
||||||
|
<textarea ref="input"
|
||||||
|
:value="value"
|
||||||
|
:required="required"
|
||||||
|
:readonly="readonly"
|
||||||
|
:pattern="pattern"
|
||||||
|
:autocomplete="autocomplete"
|
||||||
|
@input="$emit('input', $event.target.value)"
|
||||||
|
@focus="focused = true"
|
||||||
|
@blur="focused = false">
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="text"><slot name="text"></slot></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
const getPasswordStrength = require('syuilo-password-strength');
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
pattern: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
autocomplete: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
focused: false,
|
||||||
|
passwordStrength: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filled(): boolean {
|
||||||
|
return this.value != '' && this.value != null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$refs.input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark, fill)
|
||||||
|
margin 42px 0 32px 0
|
||||||
|
|
||||||
|
> .input
|
||||||
|
padding 12px
|
||||||
|
|
||||||
|
if fill
|
||||||
|
background rgba(#000, 0.035)
|
||||||
|
border-radius 6px
|
||||||
|
else
|
||||||
|
&:before
|
||||||
|
content ''
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
background none
|
||||||
|
border solid 1px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
|
||||||
|
border-radius 3px
|
||||||
|
pointer-events none
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content ''
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
background none
|
||||||
|
border solid 2px $theme-color
|
||||||
|
border-radius 3px
|
||||||
|
opacity 0
|
||||||
|
transition opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
pointer-events none
|
||||||
|
|
||||||
|
> .label
|
||||||
|
position absolute
|
||||||
|
top 6px
|
||||||
|
left 12px
|
||||||
|
pointer-events none
|
||||||
|
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
|
||||||
|
transition-duration 0.3s
|
||||||
|
font-size 16px
|
||||||
|
line-height 32px
|
||||||
|
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
|
||||||
|
pointer-events none
|
||||||
|
//will-change transform
|
||||||
|
transform-origin top left
|
||||||
|
transform scale(1)
|
||||||
|
|
||||||
|
> textarea
|
||||||
|
display block
|
||||||
|
width 100%
|
||||||
|
min-height 100px
|
||||||
|
padding 0
|
||||||
|
font inherit
|
||||||
|
font-weight fill ? bold : normal
|
||||||
|
font-size 16px
|
||||||
|
color isDark ? #fff : #000
|
||||||
|
background transparent
|
||||||
|
border none
|
||||||
|
border-radius 0
|
||||||
|
outline none
|
||||||
|
box-shadow none
|
||||||
|
|
||||||
|
> .text
|
||||||
|
margin 6px 0
|
||||||
|
font-size 13px
|
||||||
|
|
||||||
|
*
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
&.focused
|
||||||
|
> .input
|
||||||
|
if fill
|
||||||
|
background rgba(#000, 0.05)
|
||||||
|
else
|
||||||
|
&:after
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
> .label
|
||||||
|
color $theme-color
|
||||||
|
|
||||||
|
&.focused
|
||||||
|
&.filled
|
||||||
|
> .input
|
||||||
|
> .label
|
||||||
|
top -24px
|
||||||
|
left 0 !important
|
||||||
|
transform scale(0.75)
|
||||||
|
|
||||||
|
.ui-textarea[data-darkmode]
|
||||||
|
&.fill
|
||||||
|
root(true, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(true, false)
|
||||||
|
|
||||||
|
.ui-textarea:not([data-darkmode])
|
||||||
|
&.fill
|
||||||
|
root(false, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(false, false)
|
||||||
|
|
||||||
|
</style>
|
@ -203,6 +203,7 @@ root(isDark)
|
|||||||
justify-content center
|
justify-content center
|
||||||
align-items center
|
align-items center
|
||||||
margin-right 10px
|
margin-right 10px
|
||||||
|
width 16px
|
||||||
|
|
||||||
> *:last-child
|
> *:last-child
|
||||||
flex 1 1 auto
|
flex 1 1 auto
|
||||||
|
@ -109,6 +109,9 @@ root(isDark)
|
|||||||
> .created-at
|
> .created-at
|
||||||
color isDark ? #606984 : #c0c0c0
|
color isDark ? #606984 : #c0c0c0
|
||||||
|
|
||||||
|
> .text
|
||||||
|
text-align left
|
||||||
|
|
||||||
.mk-welcome-timeline[data-darkmode]
|
.mk-welcome-timeline[data-darkmode]
|
||||||
root(true)
|
root(true)
|
||||||
|
|
||||||
|
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>
|
118
src/client/app/common/views/widgets/hashtags.vue
Normal file
118
src/client/app/common/views/widgets/hashtags.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<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-move
|
||||||
|
transition transform 1s 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 wTips from './tips.vue';
|
||||||
import wDonation from './donation.vue';
|
import wDonation from './donation.vue';
|
||||||
import wNav from './nav.vue';
|
import wNav from './nav.vue';
|
||||||
|
import wHashtags from './hashtags.vue';
|
||||||
|
|
||||||
Vue.component('mkw-analog-clock', wAnalogClock);
|
Vue.component('mkw-analog-clock', wAnalogClock);
|
||||||
Vue.component('mkw-nav', wNav);
|
Vue.component('mkw-nav', wNav);
|
||||||
@ -27,3 +28,4 @@ Vue.component('mkw-posts-monitor', wPostsMonitor);
|
|||||||
Vue.component('mkw-memo', wMemo);
|
Vue.component('mkw-memo', wMemo);
|
||||||
Vue.component('mkw-rss', wRss);
|
Vue.component('mkw-rss', wRss);
|
||||||
Vue.component('mkw-version', wVersion);
|
Vue.component('mkw-version', wVersion);
|
||||||
|
Vue.component('mkw-hashtags', wHashtags);
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
declare const _HOST_: string;
|
declare const _HOST_: string;
|
||||||
declare const _HOSTNAME_: string;
|
declare const _HOSTNAME_: string;
|
||||||
declare const _URL_: string;
|
declare const _URL_: string;
|
||||||
|
declare const _NAME_: string;
|
||||||
|
declare const _DESCRIPTION_: string;
|
||||||
declare const _API_URL_: string;
|
declare const _API_URL_: string;
|
||||||
declare const _WS_URL_: string;
|
declare const _WS_URL_: string;
|
||||||
declare const _DOCS_URL_: string;
|
declare const _DOCS_URL_: string;
|
||||||
@ -17,10 +19,13 @@ declare const _VERSION_: string;
|
|||||||
declare const _CODENAME_: string;
|
declare const _CODENAME_: string;
|
||||||
declare const _LICENSE_: string;
|
declare const _LICENSE_: string;
|
||||||
declare const _GOOGLE_MAPS_API_KEY_: string;
|
declare const _GOOGLE_MAPS_API_KEY_: string;
|
||||||
|
declare const _WELCOME_BG_URL_: string;
|
||||||
|
|
||||||
export const host = _HOST_;
|
export const host = _HOST_;
|
||||||
export const hostname = _HOSTNAME_;
|
export const hostname = _HOSTNAME_;
|
||||||
export const url = _URL_;
|
export const url = _URL_;
|
||||||
|
export const name = _NAME_;
|
||||||
|
export const description = _DESCRIPTION_;
|
||||||
export const apiUrl = _API_URL_;
|
export const apiUrl = _API_URL_;
|
||||||
export const wsUrl = _WS_URL_;
|
export const wsUrl = _WS_URL_;
|
||||||
export const docsUrl = _DOCS_URL_;
|
export const docsUrl = _DOCS_URL_;
|
||||||
@ -37,3 +42,4 @@ export const version = _VERSION_;
|
|||||||
export const codename = _CODENAME_;
|
export const codename = _CODENAME_;
|
||||||
export const license = _LICENSE_;
|
export const license = _LICENSE_;
|
||||||
export const googleMapsApiKey = _GOOGLE_MAPS_API_KEY_;
|
export const googleMapsApiKey = _GOOGLE_MAPS_API_KEY_;
|
||||||
|
export const welcomeBgUrl = _WELCOME_BG_URL_;
|
||||||
|
@ -33,6 +33,7 @@ import MkHomeCustomize from './views/pages/home-customize.vue';
|
|||||||
import MkMessagingRoom from './views/pages/messaging-room.vue';
|
import MkMessagingRoom from './views/pages/messaging-room.vue';
|
||||||
import MkNote from './views/pages/note.vue';
|
import MkNote from './views/pages/note.vue';
|
||||||
import MkSearch from './views/pages/search.vue';
|
import MkSearch from './views/pages/search.vue';
|
||||||
|
import MkTag from './views/pages/tag.vue';
|
||||||
import MkOthello from './views/pages/othello.vue';
|
import MkOthello from './views/pages/othello.vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,6 +61,7 @@ init(async (launch) => {
|
|||||||
{ path: '/i/lists/:list', component: MkUserList },
|
{ path: '/i/lists/:list', component: MkUserList },
|
||||||
{ path: '/selectdrive', component: MkSelectDrive },
|
{ path: '/selectdrive', component: MkSelectDrive },
|
||||||
{ path: '/search', component: MkSearch },
|
{ path: '/search', component: MkSearch },
|
||||||
|
{ path: '/tags/:tag', component: MkTag },
|
||||||
{ path: '/othello', component: MkOthello },
|
{ path: '/othello', component: MkOthello },
|
||||||
{ path: '/othello/:game', component: MkOthello },
|
{ path: '/othello/:game', component: MkOthello },
|
||||||
{ path: '/@:user', component: MkUser },
|
{ path: '/@:user', component: MkUser },
|
||||||
|
@ -145,7 +145,7 @@ export default Vue.extend({
|
|||||||
(this as any).api('drive/files/update', {
|
(this as any).api('drive/files/update', {
|
||||||
fileId: this.file.id,
|
fileId: this.file.id,
|
||||||
name: name
|
name: name
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -173,7 +173,9 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteFile() {
|
deleteFile() {
|
||||||
alert('not implemented yet');
|
(this as any).api('drive/files/delete', {
|
||||||
|
fileId: this.file.id
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -118,6 +118,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
this.connection.on('file_created', this.onStreamDriveFileCreated);
|
this.connection.on('file_created', this.onStreamDriveFileCreated);
|
||||||
this.connection.on('file_updated', this.onStreamDriveFileUpdated);
|
this.connection.on('file_updated', this.onStreamDriveFileUpdated);
|
||||||
|
this.connection.on('file_deleted', this.onStreamDriveFileDeleted);
|
||||||
this.connection.on('folder_created', this.onStreamDriveFolderCreated);
|
this.connection.on('folder_created', this.onStreamDriveFolderCreated);
|
||||||
this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
|
this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
|
||||||
|
|
||||||
@ -130,6 +131,7 @@ export default Vue.extend({
|
|||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.connection.off('file_created', this.onStreamDriveFileCreated);
|
this.connection.off('file_created', this.onStreamDriveFileCreated);
|
||||||
this.connection.off('file_updated', this.onStreamDriveFileUpdated);
|
this.connection.off('file_updated', this.onStreamDriveFileUpdated);
|
||||||
|
this.connection.off('file_deleted', this.onStreamDriveFileDeleted);
|
||||||
this.connection.off('folder_created', this.onStreamDriveFolderCreated);
|
this.connection.off('folder_created', this.onStreamDriveFolderCreated);
|
||||||
this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
|
this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
|
||||||
(this as any).os.streams.driveStream.dispose(this.connectionId);
|
(this as any).os.streams.driveStream.dispose(this.connectionId);
|
||||||
@ -167,6 +169,10 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onStreamDriveFileDeleted(fileId) {
|
||||||
|
this.removeFile(fileId);
|
||||||
|
},
|
||||||
|
|
||||||
onStreamDriveFolderCreated(folder) {
|
onStreamDriveFolderCreated(folder) {
|
||||||
this.addFolder(folder, true);
|
this.addFolder(folder, true);
|
||||||
},
|
},
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
<option value="post-form">%i18n:common.widgets.post-form%</option>
|
<option value="post-form">%i18n:common.widgets.post-form%</option>
|
||||||
<option value="messaging">%i18n:common.widgets.messaging%</option>
|
<option value="messaging">%i18n:common.widgets.messaging%</option>
|
||||||
<option value="memo">%i18n:common.widgets.memo%</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="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
|
||||||
<option value="server">%i18n:common.widgets.server%</option>
|
<option value="server">%i18n:common.widgets.server%</option>
|
||||||
<option value="donation">%i18n:common.widgets.donation%</option>
|
<option value="donation">%i18n:common.widgets.donation%</option>
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
<mk-poll v-if="p.poll" :note="p"/>
|
<mk-poll v-if="p.poll" :note="p"/>
|
||||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
||||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
<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>
|
||||||
<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>
|
<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>
|
<div class="map" v-if="p.geo" ref="map"></div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mk-note-preview" :title="title">
|
<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">
|
<div class="main">
|
||||||
<mk-note-header class="header" :note="note" :mini="true"/>
|
<mk-note-header class="header" :note="note" :mini="true"/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
@ -15,7 +15,17 @@ import Vue from 'vue';
|
|||||||
import dateStringify from '../../../common/scripts/date-stringify';
|
import dateStringify from '../../../common/scripts/date-stringify';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: ['note'],
|
props: {
|
||||||
|
note: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
mini: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
title(): string {
|
title(): string {
|
||||||
return dateStringify(this.note.createdAt);
|
return dateStringify(this.note.createdAt);
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
<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>
|
||||||
<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>
|
<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>
|
<div class="map" v-if="p.geo" ref="map"></div>
|
||||||
|
@ -50,6 +50,7 @@ import * as XDraggable from 'vuedraggable';
|
|||||||
import getKao from '../../../common/scripts/get-kao';
|
import getKao from '../../../common/scripts/get-kao';
|
||||||
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
|
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
|
||||||
import parse from '../../../../../text/parse';
|
import parse from '../../../../../text/parse';
|
||||||
|
import { host } from '../../../config';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -129,6 +130,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
// 自分は除外
|
// 自分は除外
|
||||||
if (this.$store.state.i.username == x.username && x.host == null) return;
|
if (this.$store.state.i.username == x.username && x.host == null) return;
|
||||||
|
if (this.$store.state.i.username == x.username && x.host == host) return;
|
||||||
|
|
||||||
// 重複は除外
|
// 重複は除外
|
||||||
if (this.text.indexOf(`${mention} `) != -1) return;
|
if (this.text.indexOf(`${mention} `) != -1) return;
|
||||||
|
@ -17,7 +17,11 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onSubmit() {
|
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>
|
</div>
|
||||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
<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>
|
||||||
<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>
|
<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">
|
<div class="renote" v-if="p.renote">
|
||||||
<mk-note-preview :note="p.renote"/>
|
<mk-note-preview :note="p.renote" :mini="true"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
|
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq">
|
<div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq">
|
||||||
<template v-if="edit">
|
<template v-if="edit">
|
||||||
<header>
|
<header>
|
||||||
<select v-model="widgetAdderSelected">
|
<select v-model="widgetAdderSelected" @change="addWidget">
|
||||||
<option value="profile">%i18n:common.widgets.profile%</option>
|
<option value="profile">%i18n:common.widgets.profile%</option>
|
||||||
<option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
|
<option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
|
||||||
<option value="calendar">%i18n:common.widgets.calendar%</option>
|
<option value="calendar">%i18n:common.widgets.calendar%</option>
|
||||||
@ -23,27 +23,23 @@
|
|||||||
<option value="post-form">%i18n:common.widgets.post-form%</option>
|
<option value="post-form">%i18n:common.widgets.post-form%</option>
|
||||||
<option value="messaging">%i18n:common.widgets.messaging%</option>
|
<option value="messaging">%i18n:common.widgets.messaging%</option>
|
||||||
<option value="memo">%i18n:common.widgets.memo%</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="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
|
||||||
<option value="server">%i18n:common.widgets.server%</option>
|
<option value="server">%i18n:common.widgets.server%</option>
|
||||||
<option value="donation">%i18n:common.widgets.donation%</option>
|
<option value="donation">%i18n:common.widgets.donation%</option>
|
||||||
<option value="nav">%i18n:common.widgets.nav%</option>
|
<option value="nav">%i18n:common.widgets.nav%</option>
|
||||||
<option value="tips">%i18n:common.widgets.tips%</option>
|
<option value="tips">%i18n:common.widgets.tips%</option>
|
||||||
</select>
|
</select>
|
||||||
<button @click="addWidget">%i18n:@add%</button>
|
|
||||||
</header>
|
</header>
|
||||||
<x-draggable
|
<x-draggable
|
||||||
:list="column.widgets"
|
:list="column.widgets"
|
||||||
:options="{ handle: '.handle', animation: 150 }"
|
:options="{ animation: 150 }"
|
||||||
@sort="onWidgetSort"
|
@sort="onWidgetSort"
|
||||||
>
|
>
|
||||||
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id">
|
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="widgetFunc(widget.id)">
|
||||||
<header>
|
<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
|
||||||
<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"/>
|
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</x-draggable>
|
</x-draggable>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -141,6 +137,13 @@ export default Vue.extend({
|
|||||||
|
|
||||||
root(isDark)
|
root(isDark)
|
||||||
.gqpwvtwtprsbmnssnbicggtwqhmylhnq
|
.gqpwvtwtprsbmnssnbicggtwqhmylhnq
|
||||||
|
> header
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
> *
|
||||||
|
width 100%
|
||||||
|
padding 4px
|
||||||
|
|
||||||
.widget, .customize-container
|
.widget, .customize-container
|
||||||
margin 8px
|
margin 8px
|
||||||
|
|
||||||
@ -148,7 +151,21 @@ root(isDark)
|
|||||||
margin-top 0
|
margin-top 0
|
||||||
|
|
||||||
.customize-container
|
.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
|
> header
|
||||||
color isDark ? #fff : #000
|
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>
|
@ -1,59 +1,80 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mk-welcome">
|
<div class="mk-welcome">
|
||||||
|
<img ref="pointer" class="pointer" src="/assets/pointer.png" alt="">
|
||||||
<button @click="dark">
|
<button @click="dark">
|
||||||
<template v-if="$store.state.device.darkmode">%fa:moon%</template>
|
<template v-if="$store.state.device.darkmode">%fa:moon%</template>
|
||||||
<template v-else>%fa:R moon%</template>
|
<template v-else>%fa:R moon%</template>
|
||||||
</button>
|
</button>
|
||||||
<main v-if="about" class="about">
|
<div class="body" :style="{ backgroundImage: `url('${ welcomeBgUrl }')` }">
|
||||||
<article>
|
<div class="container">
|
||||||
<h1>%i18n:common.about-title%</h1>
|
<main>
|
||||||
<p v-html="'%i18n:common.about%'"></p>
|
<div class="about">
|
||||||
<span class="gotit" @click="about = false">%i18n:@gotit%</span>
|
<h1 v-if="name">{{ name }}</h1>
|
||||||
</article>
|
<h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="Misskey"></h1>
|
||||||
|
<p class="powerd-by" v-if="name">powerd by <b>Misskey</b></p>
|
||||||
|
<p class="desc" v-html="description || '%i18n:common.about%'"></p>
|
||||||
|
<a ref="signup" @click="signup">%i18n:@signup%</a>
|
||||||
|
</div>
|
||||||
|
<div class="login">
|
||||||
|
<mk-signin/>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<main v-else class="index">
|
<div class="info">
|
||||||
<img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="Misskey">
|
<span>%i18n:common.misskey% <b>{{ host }}</b></span>
|
||||||
<p class="desc"><b>%i18n:common.misskey%</b> - <span @click="about = true">%i18n:@about%</span></p>
|
<span class="stats" v-if="stats">
|
||||||
<p class="account">
|
<span>%fa:user% {{ stats.originalUsersCount | number }}</span>
|
||||||
<button class="signup" @click="signup">%i18n:@signup-button%</button>
|
<span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
|
||||||
<button class="signin" @click="signin">%i18n:@signin-button%</button>
|
</span>
|
||||||
</p>
|
</div>
|
||||||
|
<mk-nav class="nav"/>
|
||||||
|
</div>
|
||||||
|
<mk-forkit class="forkit"/>
|
||||||
|
<img src="assets/title.dark.svg" alt="Misskey">
|
||||||
|
</div>
|
||||||
<div class="tl">
|
<div class="tl">
|
||||||
<header>%fa:comments R% %i18n:@timeline%<div><span></span><span></span><span></span></div></header>
|
|
||||||
<mk-welcome-timeline/>
|
<mk-welcome-timeline/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
<mk-forkit/>
|
|
||||||
<footer>
|
|
||||||
<div>
|
|
||||||
<mk-nav :class="$style.nav"/>
|
|
||||||
<p class="c">{{ copyright }}</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
<modal name="signup" width="500px" height="auto" scrollable>
|
<modal name="signup" width="500px" height="auto" scrollable>
|
||||||
<header :class="$style.signupFormHeader">%i18n:@signup%</header>
|
<header :class="$style.signupFormHeader">%i18n:@signup%</header>
|
||||||
<mk-signup :class="$style.signupForm"/>
|
<mk-signup :class="$style.signupForm"/>
|
||||||
</modal>
|
</modal>
|
||||||
<modal name="signin" width="500px" height="auto" scrollable>
|
|
||||||
<header :class="$style.signinFormHeader">%i18n:@signin%</header>
|
|
||||||
<mk-signin :class="$style.signinForm"/>
|
|
||||||
</modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { copyright } from '../../../config';
|
import { host, name, description, copyright, welcomeBgUrl } from '../../../config';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
about: false,
|
stats: null,
|
||||||
copyright
|
copyright,
|
||||||
|
welcomeBgUrl,
|
||||||
|
host,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
pointerInterval: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
(this as any).api('stats').then(stats => {
|
||||||
|
this.stats = stats;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.point();
|
||||||
|
this.pointerInterval = setInterval(this.point, 100);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
clearInterval(this.pointerInterval);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
point() {
|
||||||
|
const x = this.$refs.signup.getBoundingClientRect();
|
||||||
|
this.$refs.pointer.style.top = x.top + x.height + 'px';
|
||||||
|
this.$refs.pointer.style.left = x.left + 'px';
|
||||||
|
},
|
||||||
signup() {
|
signup() {
|
||||||
this.$modal.show('signup');
|
this.$modal.show('signup');
|
||||||
},
|
},
|
||||||
@ -80,13 +101,20 @@ export default Vue.extend({
|
|||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
@import '~const.styl'
|
@import '~const.styl'
|
||||||
|
|
||||||
@import url(https://fonts.googleapis.com/earlyaccess/notosansjp.css);
|
|
||||||
|
|
||||||
root(isDark)
|
root(isDark)
|
||||||
|
display flex
|
||||||
min-height 100vh
|
min-height 100vh
|
||||||
background-image isDark ? url('/assets/welcome-bg.dark.svg') : url('/assets/welcome-bg.light.svg')
|
|
||||||
background-size cover
|
> .pointer
|
||||||
background-position center
|
display block
|
||||||
|
position absolute
|
||||||
|
z-index 1
|
||||||
|
top 0
|
||||||
|
right 0
|
||||||
|
width 180px
|
||||||
|
margin 0 0 0 -180px
|
||||||
|
transform rotateY(180deg) translateX(-10px) translateY(-48px)
|
||||||
|
pointer-events none
|
||||||
|
|
||||||
> button
|
> button
|
||||||
position fixed
|
position fixed
|
||||||
@ -95,141 +123,118 @@ root(isDark)
|
|||||||
left 0
|
left 0
|
||||||
padding 16px
|
padding 16px
|
||||||
font-size 18px
|
font-size 18px
|
||||||
color isDark ? #fff : #555
|
color #fff
|
||||||
|
|
||||||
> main
|
display none // TODO
|
||||||
|
|
||||||
|
> .body
|
||||||
flex 1
|
flex 1
|
||||||
padding 64px 0 0 0
|
padding 64px 0 0 0
|
||||||
text-align center
|
text-align center
|
||||||
|
background #578394
|
||||||
|
background-position center
|
||||||
|
background-size cover
|
||||||
|
|
||||||
&.about
|
&:before
|
||||||
font-family 'Noto Sans JP'
|
content ''
|
||||||
color isDark ? #fff : #627574
|
display block
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
bottom 0
|
||||||
|
background rgba(#000, 0.5)
|
||||||
|
|
||||||
> article
|
> .forkit
|
||||||
max-width 700px
|
|
||||||
margin 42px auto 0 auto
|
|
||||||
padding 64px 82px
|
|
||||||
background isDark ? #282C37 : #fff
|
|
||||||
box-shadow 0 8px 32px rgba(#000, 0.15)
|
|
||||||
|
|
||||||
> h1
|
|
||||||
margin 0
|
|
||||||
font-weight 900
|
|
||||||
|
|
||||||
> p
|
|
||||||
margin 20px 0
|
|
||||||
line-height 2em
|
|
||||||
|
|
||||||
> .gotit
|
|
||||||
color $theme-color
|
|
||||||
cursor pointer
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
text-decoration underline
|
|
||||||
|
|
||||||
&.index
|
|
||||||
color isDark ? #9aa4b3 : #555
|
|
||||||
|
|
||||||
> img
|
|
||||||
width 350px
|
|
||||||
|
|
||||||
> .desc
|
|
||||||
margin -12px 0 24px 0
|
|
||||||
color isDark ? #fff : #555
|
|
||||||
|
|
||||||
> span
|
|
||||||
color $theme-color
|
|
||||||
cursor pointer
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
text-decoration underline
|
|
||||||
|
|
||||||
> .account
|
|
||||||
margin 8px 0
|
|
||||||
line-height 2em
|
|
||||||
|
|
||||||
button
|
|
||||||
padding 8px 16px
|
|
||||||
font-size inherit
|
|
||||||
|
|
||||||
.signup
|
|
||||||
color $theme-color
|
|
||||||
border solid 2px $theme-color
|
|
||||||
border-radius 4px
|
|
||||||
|
|
||||||
&:focus
|
|
||||||
box-shadow 0 0 0 3px rgba($theme-color, 0.2)
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
color $theme-color-foreground
|
|
||||||
background $theme-color
|
|
||||||
|
|
||||||
&:active
|
|
||||||
color $theme-color-foreground
|
|
||||||
background darken($theme-color, 10%)
|
|
||||||
border-color darken($theme-color, 10%)
|
|
||||||
|
|
||||||
.signin
|
|
||||||
&:hover
|
|
||||||
color isDark ? #fff : #000
|
|
||||||
|
|
||||||
> .tl
|
|
||||||
margin 32px auto 0 auto
|
|
||||||
width 410px
|
|
||||||
text-align left
|
|
||||||
background isDark ? #313543 : #fff
|
|
||||||
border-radius 8px
|
|
||||||
box-shadow 0 8px 32px rgba(#000, 0.15)
|
|
||||||
overflow hidden
|
|
||||||
|
|
||||||
> header
|
|
||||||
z-index 1
|
|
||||||
padding 12px 16px
|
|
||||||
color isDark ? #e3e5e8 : #888d94
|
|
||||||
box-shadow 0 1px 0px rgba(#000, 0.1)
|
|
||||||
|
|
||||||
> div
|
|
||||||
position absolute
|
position absolute
|
||||||
top 0
|
top 0
|
||||||
right 0
|
right 0
|
||||||
padding inherit
|
|
||||||
|
|
||||||
> span
|
> img
|
||||||
display inline-block
|
position absolute
|
||||||
height 11px
|
bottom 16px
|
||||||
width 11px
|
right 16px
|
||||||
margin-left 6px
|
width 150px
|
||||||
border-radius 100%
|
|
||||||
vertical-align middle
|
|
||||||
|
|
||||||
&:nth-child(1)
|
> .container
|
||||||
background #5BCC8B
|
$aboutWidth = 380px
|
||||||
|
$loginWidth = 340px
|
||||||
|
$width = $aboutWidth + $loginWidth
|
||||||
|
|
||||||
&:nth-child(2)
|
> main
|
||||||
background #E6BB46
|
display flex
|
||||||
|
margin auto
|
||||||
|
width $width
|
||||||
|
border-radius 8px
|
||||||
|
overflow hidden
|
||||||
|
box-shadow 0 2px 8px rgba(#000, 0.3)
|
||||||
|
|
||||||
&:nth-child(3)
|
> .about
|
||||||
background #DF7065
|
width $aboutWidth
|
||||||
|
color #444
|
||||||
|
background #fff
|
||||||
|
|
||||||
> .mk-welcome-timeline
|
> h1
|
||||||
max-height 350px
|
margin 0 0 16px 0
|
||||||
overflow auto
|
padding 32px 32px 0 32px
|
||||||
|
color #444
|
||||||
|
|
||||||
> footer
|
> img
|
||||||
font-size 12px
|
width 170px
|
||||||
color isDark ? #949ea5 : #737c82
|
vertical-align bottom
|
||||||
|
|
||||||
> div
|
> .powerd-by
|
||||||
margin 0 auto
|
margin 16px
|
||||||
padding 64px
|
|
||||||
text-align center
|
|
||||||
|
|
||||||
> .c
|
|
||||||
margin 16px 0 0 0
|
|
||||||
font-size 10px
|
|
||||||
opacity 0.7
|
opacity 0.7
|
||||||
|
|
||||||
|
> .desc
|
||||||
|
margin 0
|
||||||
|
padding 0 32px 16px 32px
|
||||||
|
|
||||||
|
> a
|
||||||
|
display inline-block
|
||||||
|
margin 0 0 32px 0
|
||||||
|
font-weight bold
|
||||||
|
|
||||||
|
> .login
|
||||||
|
width $loginWidth
|
||||||
|
padding 16px 32px 32px 32px
|
||||||
|
background #f5f5f5
|
||||||
|
|
||||||
|
> .info
|
||||||
|
margin 16px auto
|
||||||
|
padding 12px
|
||||||
|
width $width
|
||||||
|
font-size 14px
|
||||||
|
color #fff
|
||||||
|
background rgba(#000, 0.2)
|
||||||
|
border-radius 8px
|
||||||
|
|
||||||
|
> .stats
|
||||||
|
margin-left 16px
|
||||||
|
padding-left 16px
|
||||||
|
border-left solid 1px #fff
|
||||||
|
|
||||||
|
> *
|
||||||
|
margin-right 16px
|
||||||
|
|
||||||
|
> .nav
|
||||||
|
display block
|
||||||
|
margin 16px 0
|
||||||
|
font-size 14px
|
||||||
|
color #fff
|
||||||
|
|
||||||
|
> .tl
|
||||||
|
margin 0
|
||||||
|
width 410px
|
||||||
|
height 100vh
|
||||||
|
text-align left
|
||||||
|
background isDark ? #313543 : #fff
|
||||||
|
|
||||||
|
> *
|
||||||
|
max-height 100%
|
||||||
|
overflow auto
|
||||||
|
|
||||||
.mk-welcome[data-darkmode]
|
.mk-welcome[data-darkmode]
|
||||||
root(true)
|
root(true)
|
||||||
|
|
||||||
|
@ -2,17 +2,11 @@
|
|||||||
* Mobile Client
|
* Mobile Client
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Vue from 'vue';
|
|
||||||
import VueRouter from 'vue-router';
|
import VueRouter from 'vue-router';
|
||||||
|
|
||||||
import { MdCard, MdButton, MdField, MdMenu, MdList, MdSwitch, MdSubheader, MdDialog, MdDialogAlert, MdRadio } from 'vue-material/dist/components';
|
|
||||||
import 'vue-material/dist/vue-material.min.css';
|
|
||||||
import 'vue-material/dist/theme/default.css';
|
|
||||||
|
|
||||||
// Style
|
// Style
|
||||||
import './style.styl';
|
import './style.styl';
|
||||||
import '../../element.scss';
|
import '../../element.scss';
|
||||||
import '../../md.scss';
|
|
||||||
|
|
||||||
import init from '../init';
|
import init from '../init';
|
||||||
|
|
||||||
@ -42,17 +36,7 @@ import MkUserLists from './views/pages/user-lists.vue';
|
|||||||
import MkUserList from './views/pages/user-list.vue';
|
import MkUserList from './views/pages/user-list.vue';
|
||||||
import MkSettings from './views/pages/settings.vue';
|
import MkSettings from './views/pages/settings.vue';
|
||||||
import MkOthello from './views/pages/othello.vue';
|
import MkOthello from './views/pages/othello.vue';
|
||||||
|
import MkTag from './views/pages/tag.vue';
|
||||||
Vue.use(MdCard);
|
|
||||||
Vue.use(MdButton);
|
|
||||||
Vue.use(MdField);
|
|
||||||
Vue.use(MdMenu);
|
|
||||||
Vue.use(MdList);
|
|
||||||
Vue.use(MdSwitch);
|
|
||||||
Vue.use(MdSubheader);
|
|
||||||
Vue.use(MdDialog);
|
|
||||||
Vue.use(MdDialogAlert);
|
|
||||||
Vue.use(MdRadio);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* init
|
* init
|
||||||
@ -88,6 +72,7 @@ init((launch) => {
|
|||||||
{ path: '/i/drive/file/:file', component: MkDrive },
|
{ path: '/i/drive/file/:file', component: MkDrive },
|
||||||
{ path: '/selectdrive', component: MkSelectDrive },
|
{ path: '/selectdrive', component: MkSelectDrive },
|
||||||
{ path: '/search', component: MkSearch },
|
{ path: '/search', component: MkSearch },
|
||||||
|
{ path: '/tags/:tag', component: MkTag },
|
||||||
{ path: '/othello', name: 'othello', component: MkOthello },
|
{ path: '/othello', name: 'othello', component: MkOthello },
|
||||||
{ path: '/othello/:game', component: MkOthello },
|
{ path: '/othello/:game', component: MkOthello },
|
||||||
{ path: '/@:user', component: MkUser },
|
{ path: '/@:user', component: MkUser },
|
||||||
|
@ -10,9 +10,6 @@ html
|
|||||||
height 100%
|
height 100%
|
||||||
background #ececed !important
|
background #ececed !important
|
||||||
|
|
||||||
// for md
|
|
||||||
transition none !important
|
|
||||||
|
|
||||||
&[data-darkmode]
|
&[data-darkmode]
|
||||||
background #191B22 !important
|
background #191B22 !important
|
||||||
|
|
||||||
|
@ -34,15 +34,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<div>
|
<div>
|
||||||
<a :href="`${file.url}?download`" :download="file.name">
|
<a :href="`${file.url}?download`" :download="file.name">%fa:download%%i18n:@download%</a>
|
||||||
%fa:download%%i18n:@download%
|
<button @click="rename">%fa:pencil-alt%%i18n:@rename%</button>
|
||||||
</a>
|
<button @click="move">%fa:R folder-open%%i18n:@move%</button>
|
||||||
<button @click="rename">
|
<button @click="del">%fa:trash-alt R%%i18n:@delete%</button>
|
||||||
%fa:pencil-alt%%i18n:@rename%
|
|
||||||
</button>
|
|
||||||
<button @click="move">
|
|
||||||
%fa:R folder-open%%i18n:@move%
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="exif" v-show="exif">
|
<div class="exif" v-show="exif">
|
||||||
@ -112,6 +107,13 @@ export default Vue.extend({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
del() {
|
||||||
|
(this as any).api('drive/files/delete', {
|
||||||
|
fileId: this.file.id
|
||||||
|
}).then(() => {
|
||||||
|
this.browser.cd(this.file.folderId, true);
|
||||||
|
});
|
||||||
|
},
|
||||||
showCreatedAt() {
|
showCreatedAt() {
|
||||||
alert(new Date(this.file.createdAt).toLocaleString());
|
alert(new Date(this.file.createdAt).toLocaleString());
|
||||||
},
|
},
|
||||||
|
@ -100,6 +100,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
this.connection.on('file_created', this.onStreamDriveFileCreated);
|
this.connection.on('file_created', this.onStreamDriveFileCreated);
|
||||||
this.connection.on('file_updated', this.onStreamDriveFileUpdated);
|
this.connection.on('file_updated', this.onStreamDriveFileUpdated);
|
||||||
|
this.connection.on('file_deleted', this.onStreamDriveFileDeleted);
|
||||||
this.connection.on('folder_created', this.onStreamDriveFolderCreated);
|
this.connection.on('folder_created', this.onStreamDriveFolderCreated);
|
||||||
this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
|
this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
|
||||||
|
|
||||||
@ -118,6 +119,7 @@ export default Vue.extend({
|
|||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.connection.off('file_created', this.onStreamDriveFileCreated);
|
this.connection.off('file_created', this.onStreamDriveFileCreated);
|
||||||
this.connection.off('file_updated', this.onStreamDriveFileUpdated);
|
this.connection.off('file_updated', this.onStreamDriveFileUpdated);
|
||||||
|
this.connection.off('file_deleted', this.onStreamDriveFileDeleted);
|
||||||
this.connection.off('folder_created', this.onStreamDriveFolderCreated);
|
this.connection.off('folder_created', this.onStreamDriveFolderCreated);
|
||||||
this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
|
this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
|
||||||
(this as any).os.streams.driveStream.dispose(this.connectionId);
|
(this as any).os.streams.driveStream.dispose(this.connectionId);
|
||||||
@ -136,6 +138,10 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onStreamDriveFileDeleted(fileId) {
|
||||||
|
this.removeFile(fileId);
|
||||||
|
},
|
||||||
|
|
||||||
onStreamDriveFolderCreated(folder) {
|
onStreamDriveFolderCreated(folder) {
|
||||||
this.addFolder(folder, true);
|
this.addFolder(folder, true);
|
||||||
},
|
},
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
<mk-note-html v-if="p.text" :text="p.text" :i="$store.state.i"/>
|
<mk-note-html v-if="p.text" :text="p.text" :i="$store.state.i"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
<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>
|
||||||
<div class="media" v-if="p.media.length > 0">
|
<div class="media" v-if="p.media.length > 0">
|
||||||
<mk-media-list :media-list="p.media" :raw="true"/>
|
<mk-media-list :media-list="p.media" :raw="true"/>
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
<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>
|
||||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
<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>
|
<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>
|
||||||
|
@ -46,6 +46,7 @@ import * as XDraggable from 'vuedraggable';
|
|||||||
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
|
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
|
||||||
import getKao from '../../../common/scripts/get-kao';
|
import getKao from '../../../common/scripts/get-kao';
|
||||||
import parse from '../../../../../text/parse';
|
import parse from '../../../../../text/parse';
|
||||||
|
import { host } from '../../../config';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -123,6 +124,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
// 自分は除外
|
// 自分は除外
|
||||||
if (this.$store.state.i.username == x.username && x.host == null) return;
|
if (this.$store.state.i.username == x.username && x.host == null) return;
|
||||||
|
if (this.$store.state.i.username == x.username && x.host == host) return;
|
||||||
|
|
||||||
// 重複は除外
|
// 重複は除外
|
||||||
if (this.text.indexOf(`${mention} `) != -1) return;
|
if (this.text.indexOf(`${mention} `) != -1) return;
|
||||||
|
@ -1,132 +1,84 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<span slot="header">%fa:cog%%i18n:@settings%</span>
|
<span slot="header">%fa:cog%%i18n:@settings%</span>
|
||||||
<main>
|
<main :data-darkmode="$store.state.device.darkmode">
|
||||||
<p v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p>
|
<div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<x-profile/>
|
<x-profile/>
|
||||||
|
|
||||||
<md-card>
|
<ui-card>
|
||||||
<md-card-header>
|
<div slot="title">%fa:palette% %i18n:@design%</div>
|
||||||
<div class="md-title">%fa:palette% %i18n:@design%</div>
|
|
||||||
</md-card-header>
|
<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
|
||||||
|
<ui-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</ui-switch>
|
||||||
|
|
||||||
<md-card-content>
|
|
||||||
<div>
|
<div>
|
||||||
<md-switch v-model="darkmode">%i18n:@dark-mode%</md-switch>
|
<div>%i18n:@timeline%</div>
|
||||||
|
<ui-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</ui-switch>
|
||||||
|
<ui-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</ui-switch>
|
||||||
|
<ui-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<md-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</md-switch>
|
<div>%i18n:@post-style%</div>
|
||||||
|
<ui-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</ui-radio>
|
||||||
|
<ui-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</ui-radio>
|
||||||
</div>
|
</div>
|
||||||
|
</ui-card>
|
||||||
|
|
||||||
<div>
|
<ui-card>
|
||||||
<div class="md-body-2">%i18n:@timeline%</div>
|
<div slot="title">%fa:cog% %i18n:@behavior%</div>
|
||||||
|
<ui-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch>
|
||||||
|
<ui-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</ui-switch>
|
||||||
|
<ui-switch v-model="loadRawImages">%i18n:@load-raw-images%</ui-switch>
|
||||||
|
<ui-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</ui-switch>
|
||||||
|
<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
|
||||||
|
</ui-card>
|
||||||
|
|
||||||
<div>
|
<ui-card>
|
||||||
<md-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</md-switch>
|
<div slot="title">%fa:language% %i18n:@lang%</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<ui-select v-model="lang" placeholder="%i18n:@auto%">
|
||||||
<md-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</md-switch>
|
<optgroup label="%i18n:@recommended%">
|
||||||
</div>
|
<option value="">%i18n:@auto%</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
<div>
|
<optgroup label="%i18n:@specify-language%">
|
||||||
<md-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</md-switch>
|
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
|
||||||
</div>
|
</optgroup>
|
||||||
</div>
|
</ui-select>
|
||||||
|
<span>%fa:info-circle% %i18n:@lang-tip%</span>
|
||||||
|
</ui-card>
|
||||||
|
|
||||||
<div>
|
<ui-card>
|
||||||
<div class="md-body-2">%i18n:@post-style%</div>
|
<div slot="title">%fa:B twitter% %i18n:@twitter%</div>
|
||||||
|
|
||||||
<md-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</md-radio>
|
|
||||||
<md-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</md-radio>
|
|
||||||
</div>
|
|
||||||
</md-card-content>
|
|
||||||
</md-card>
|
|
||||||
|
|
||||||
<md-card>
|
|
||||||
<md-card-header>
|
|
||||||
<div class="md-title">%fa:cog% %i18n:@behavior%</div>
|
|
||||||
</md-card-header>
|
|
||||||
|
|
||||||
<md-card-content>
|
|
||||||
<div>
|
|
||||||
<md-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</md-switch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<md-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</md-switch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<md-switch v-model="loadRawImages">%i18n:@load-raw-images%</md-switch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<md-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</md-switch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<md-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</md-switch>
|
|
||||||
</div>
|
|
||||||
</md-card-content>
|
|
||||||
</md-card>
|
|
||||||
|
|
||||||
<md-card>
|
|
||||||
<md-card-header>
|
|
||||||
<div class="md-title">%fa:language% %i18n:@lang%</div>
|
|
||||||
</md-card-header>
|
|
||||||
|
|
||||||
<md-card-content>
|
|
||||||
<md-field>
|
|
||||||
<md-select v-model="lang" placeholder="%i18n:@auto%">
|
|
||||||
<md-optgroup label="%i18n:@recommended%">
|
|
||||||
<md-option value="">%i18n:@auto%</md-option>
|
|
||||||
</md-optgroup>
|
|
||||||
|
|
||||||
<md-optgroup label="%i18n:@specify-language%">
|
|
||||||
<md-option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</md-option>
|
|
||||||
</md-optgroup>
|
|
||||||
</md-select>
|
|
||||||
</md-field>
|
|
||||||
<span class="md-helper-text">%fa:info-circle% %i18n:@lang-tip%</span>
|
|
||||||
</md-card-content>
|
|
||||||
</md-card>
|
|
||||||
|
|
||||||
<md-card>
|
|
||||||
<md-card-header>
|
|
||||||
<div class="md-title">%fa:B twitter% %i18n:@twitter%</div>
|
|
||||||
</md-card-header>
|
|
||||||
|
|
||||||
<md-card-content>
|
|
||||||
<p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
|
<p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
|
||||||
<p>
|
<p>
|
||||||
<a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
|
<a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
|
||||||
<span v-if="$store.state.i.twitter"> or </span>
|
<span v-if="$store.state.i.twitter"> or </span>
|
||||||
<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a>
|
<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a>
|
||||||
</p>
|
</p>
|
||||||
</md-card-content>
|
</ui-card>
|
||||||
</md-card>
|
|
||||||
|
|
||||||
<md-card>
|
<ui-card>
|
||||||
<md-card-header>
|
<div slot="title">%fa:sync-alt% %i18n:@update%</div>
|
||||||
<div class="md-title">%fa:sync-alt% %i18n:@update%</div>
|
|
||||||
</md-card-header>
|
|
||||||
|
|
||||||
<md-card-content>
|
|
||||||
<div>%i18n:@version% <i>{{ version }}</i></div>
|
<div>%i18n:@version% <i>{{ version }}</i></div>
|
||||||
<template v-if="latestVersion !== undefined">
|
<template v-if="latestVersion !== undefined">
|
||||||
<div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
|
<div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
|
||||||
</template>
|
</template>
|
||||||
<md-button class="md-raised md-primary" @click="checkForUpdate" :disabled="checkingForUpdate">
|
<ui-button @click="checkForUpdate" :disabled="checkingForUpdate">
|
||||||
<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
|
<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
|
||||||
<template v-else>%i18n:@check-for-updates%</template>
|
<template v-else>%i18n:@check-for-updates%</template>
|
||||||
</md-button>
|
</ui-button>
|
||||||
</md-card-content>
|
</ui-card>
|
||||||
</md-card>
|
|
||||||
</div>
|
</div>
|
||||||
<p><small>ver {{ version }} ({{ codename }})</small></p>
|
|
||||||
|
<footer>
|
||||||
|
<small>ver {{ version }} ({{ codename }})</small>
|
||||||
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</mk-ui>
|
</mk-ui>
|
||||||
</template>
|
</template>
|
||||||
@ -267,20 +219,22 @@ export default Vue.extend({
|
|||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
root(isDark)
|
root(isDark)
|
||||||
padding 0 16px
|
|
||||||
margin 0 auto
|
margin 0 auto
|
||||||
max-width 500px
|
max-width 500px
|
||||||
width 100%
|
width 100%
|
||||||
|
|
||||||
> div
|
> .signin-as
|
||||||
> *
|
margin 16px
|
||||||
margin-bottom 16px
|
padding 16px
|
||||||
|
|
||||||
> p
|
|
||||||
display block
|
|
||||||
margin 24px
|
|
||||||
text-align center
|
text-align center
|
||||||
color isDark ? #cad2da : #a2a9b1
|
color isDark ? #49ab63 : #2c662d
|
||||||
|
background isDark ? #273c34 : #fcfff5
|
||||||
|
box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12)
|
||||||
|
|
||||||
|
> footer
|
||||||
|
margin 16px
|
||||||
|
text-align center
|
||||||
|
color isDark ? #c9d2e0 : #888
|
||||||
|
|
||||||
main[data-darkmode]
|
main[data-darkmode]
|
||||||
root(true)
|
root(true)
|
||||||
|
@ -1,62 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<md-card>
|
<ui-card>
|
||||||
<md-card-header>
|
<div slot="title">%fa:user% %i18n:@title%</div>
|
||||||
<div class="md-title">%fa:pencil-alt% %i18n:@title%</div>
|
|
||||||
</md-card-header>
|
|
||||||
|
|
||||||
<md-card-content>
|
<ui-form :disabled="saving">
|
||||||
<md-field>
|
<ui-input v-model="name" :max="30">
|
||||||
<label>%i18n:@name%</label>
|
<span>%i18n:@name%</span>
|
||||||
<md-input v-model="name" :disabled="saving" md-counter="30"/>
|
</ui-input>
|
||||||
</md-field>
|
|
||||||
|
|
||||||
<md-field>
|
<ui-input v-model="username" readonly>
|
||||||
<label>%i18n:@account%</label>
|
<span>%i18n:@account%</span>
|
||||||
<span class="md-prefix">@</span>
|
<span slot="prefix">@</span>
|
||||||
<md-input v-model="username" readonly></md-input>
|
<span slot="suffix">@{{ host }}</span>
|
||||||
<span class="md-suffix">@{{ host }}</span>
|
</ui-input>
|
||||||
</md-field>
|
|
||||||
|
|
||||||
<md-field>
|
<ui-input v-model="location">
|
||||||
<md-icon>%fa:map-marker-alt%</md-icon>
|
<span>%i18n:@location%</span>
|
||||||
<label>%i18n:@location%</label>
|
<span slot="prefix">%fa:map-marker-alt%</span>
|
||||||
<md-input v-model="location" :disabled="saving"/>
|
</ui-input>
|
||||||
</md-field>
|
|
||||||
|
|
||||||
<md-field>
|
<ui-input v-model="birthday" type="date">
|
||||||
<md-icon>%fa:birthday-cake%</md-icon>
|
<span>%i18n:@birthday%</span>
|
||||||
<label>%i18n:@birthday%</label>
|
<span slot="prefix">%fa:birthday-cake%</span>
|
||||||
<md-input type="date" v-model="birthday" :disabled="saving"/>
|
</ui-input>
|
||||||
</md-field>
|
|
||||||
|
|
||||||
<md-field>
|
<ui-textarea v-model="description" :max="500">
|
||||||
<label>%i18n:@description%</label>
|
<span>%i18n:@description%</span>
|
||||||
<md-textarea v-model="description" :disabled="saving" md-counter="500"/>
|
</ui-textarea>
|
||||||
</md-field>
|
|
||||||
|
|
||||||
<md-field>
|
<ui-input type="file" @change="onAvatarChange">
|
||||||
<label>%i18n:@avatar%</label>
|
<span>%i18n:@avatar%</span>
|
||||||
<md-file @md-change="onAvatarChange"/>
|
<span slot="icon">%fa:image%</span>
|
||||||
</md-field>
|
<span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span>
|
||||||
|
</ui-input>
|
||||||
|
|
||||||
<md-field>
|
<ui-input type="file" @change="onBannerChange">
|
||||||
<label>%i18n:@banner%</label>
|
<span>%i18n:@banner%</span>
|
||||||
<md-file @md-change="onBannerChange"/>
|
<span slot="icon">%fa:image%</span>
|
||||||
</md-field>
|
<span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span>
|
||||||
|
</ui-input>
|
||||||
|
|
||||||
<md-dialog-alert
|
<ui-switch v-model="isCat">%i18n:@is-cat%</ui-switch>
|
||||||
:md-active.sync="uploading"
|
|
||||||
md-content="%18n:!@uploading%"/>
|
|
||||||
|
|
||||||
<div>
|
<ui-button @click="save">%i18n:@save%</ui-button>
|
||||||
<md-switch v-model="isCat">%i18n:@is-cat%</md-switch>
|
</ui-form>
|
||||||
</div>
|
</ui-card>
|
||||||
</md-card-content>
|
|
||||||
|
|
||||||
<md-card-actions>
|
|
||||||
<md-button class="md-primary" :disabled="saving" @click="save">%i18n:@save%</md-button>
|
|
||||||
</md-card-actions>
|
|
||||||
</md-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -77,7 +64,8 @@ export default Vue.extend({
|
|||||||
isBot: false,
|
isBot: false,
|
||||||
isCat: false,
|
isCat: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
uploading: false
|
avatarUploading: false,
|
||||||
|
bannerUploading: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -95,7 +83,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onAvatarChange([file]) {
|
onAvatarChange([file]) {
|
||||||
this.uploading = true;
|
this.avatarUploading = true;
|
||||||
|
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('file', file);
|
data.append('file', file);
|
||||||
@ -108,16 +96,16 @@ export default Vue.extend({
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(f => {
|
.then(f => {
|
||||||
this.avatarId = f.id;
|
this.avatarId = f.id;
|
||||||
this.uploading = false;
|
this.avatarUploading = false;
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.uploading = false;
|
this.avatarUploading = false;
|
||||||
alert('%18n:!@upload-failed%');
|
alert('%18n:!@upload-failed%');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onBannerChange([file]) {
|
onBannerChange([file]) {
|
||||||
this.uploading = true;
|
this.bannerUploading = true;
|
||||||
|
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('file', file);
|
data.append('file', file);
|
||||||
@ -130,10 +118,10 @@ export default Vue.extend({
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(f => {
|
.then(f => {
|
||||||
this.bannerId = f.id;
|
this.bannerId = f.id;
|
||||||
this.uploading = false;
|
this.bannerUploading = false;
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.uploading = false;
|
this.bannerUploading = false;
|
||||||
alert('%18n:!@upload-failed%');
|
alert('%18n:!@upload-failed%');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -1,57 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="signup">
|
<div class="signup">
|
||||||
<h1>Misskeyをはじめる</h1>
|
<h1>📦 始めましょう</h1>
|
||||||
<p>いつでも、どこからでもMisskeyを利用できます。もちろん、無料です。</p>
|
|
||||||
<div class="form">
|
|
||||||
<p>新規登録</p>
|
|
||||||
<div>
|
|
||||||
<mk-signup/>
|
<mk-signup/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
export default Vue.extend({
|
export default Vue.extend({});
|
||||||
mounted() {
|
|
||||||
document.documentElement.style.background = '#293946';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.signup
|
.signup
|
||||||
padding 16px
|
padding 32px
|
||||||
margin 0 auto
|
margin 0 auto
|
||||||
max-width 500px
|
max-width 500px
|
||||||
|
|
||||||
h1
|
h1
|
||||||
margin 0
|
margin 0
|
||||||
padding 8px
|
padding 8px 0 0 0
|
||||||
font-size 1.5em
|
font-size 1.5em
|
||||||
font-weight normal
|
font-weight bold
|
||||||
color #c3c6ca
|
color #444
|
||||||
|
|
||||||
& + p
|
|
||||||
margin 0 0 16px 0
|
|
||||||
padding 0 8px 0 8px
|
|
||||||
color #949fa9
|
|
||||||
|
|
||||||
.form
|
|
||||||
background #fff
|
|
||||||
border solid 1px rgba(#000, 0.2)
|
|
||||||
border-radius 8px
|
|
||||||
overflow hidden
|
|
||||||
|
|
||||||
> p
|
|
||||||
margin 0
|
|
||||||
padding 12px 20px
|
|
||||||
color #555
|
|
||||||
background #f5f5f5
|
|
||||||
border-bottom solid 1px #ddd
|
|
||||||
|
|
||||||
> div
|
|
||||||
padding 16px
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
81
src/client/app/mobile/views/pages/tag.vue
Normal file
81
src/client/app/mobile/views/pages/tag.vue
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<mk-ui>
|
||||||
|
<span slot="header">%fa:hashtag%{{ $route.params.tag }}</span>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<p v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p>
|
||||||
|
<mk-notes ref="timeline" :more="existMore ? more : null"/>
|
||||||
|
</main>
|
||||||
|
</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() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.fetch();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
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>
|
@ -1,28 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<div>
|
<div>
|
||||||
<h1><b>Misskey</b>へようこそ</h1>
|
<img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="Misskey">
|
||||||
<p>Twitter風ミニブログSNS、Misskeyへようこそ。共有したいことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p>
|
<p class="host">{{ host }}</p>
|
||||||
<div class="form">
|
<div class="about">
|
||||||
<p>%fa:lock% ログイン</p>
|
<h2>{{ name || 'unidentified' }}</h2>
|
||||||
<div>
|
<p v-html="description || '%i18n:common.about%'"></p>
|
||||||
<form @submit.prevent="onSubmit">
|
<router-link class="signup" to="/signup">新規登録</router-link>
|
||||||
<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/>
|
|
||||||
<input v-model="password" type="password" placeholder="パスワード" required/>
|
|
||||||
<input v-if="user && user.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/>
|
|
||||||
<button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button>
|
|
||||||
</form>
|
|
||||||
<div>
|
|
||||||
<a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="login">
|
||||||
|
<mk-signin :with-avatar="false"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="tl">
|
<div class="tl">
|
||||||
<p>%fa:comments R% タイムラインを見てみる</p>
|
|
||||||
<mk-welcome-timeline/>
|
<mk-welcome-timeline/>
|
||||||
</div>
|
</div>
|
||||||
<div class="users">
|
<div class="stats" v-if="stats">
|
||||||
<mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/>
|
<span>%fa:user% {{ stats.originalUsersCount | number }}</span>
|
||||||
|
<span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<small>{{ copyright }}</small>
|
<small>{{ copyright }}</small>
|
||||||
@ -33,107 +27,72 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { apiUrl, copyright } from '../../../config';
|
import { apiUrl, copyright, host, name, description } from '../../../config';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
signing: false,
|
|
||||||
user: null,
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
token: '',
|
|
||||||
apiUrl,
|
apiUrl,
|
||||||
copyright,
|
copyright,
|
||||||
users: []
|
stats: null,
|
||||||
|
host,
|
||||||
|
name,
|
||||||
|
description
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
created() {
|
||||||
(this as any).api('users', {
|
(this as any).api('stats').then(stats => {
|
||||||
sort: '+follower',
|
this.stats = stats;
|
||||||
limit: 20
|
|
||||||
}).then(users => {
|
|
||||||
this.users = users;
|
|
||||||
});
|
});
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onUsernameChange() {
|
|
||||||
(this as any).api('users/show', {
|
|
||||||
username: this.username
|
|
||||||
}).then(user => {
|
|
||||||
this.user = user;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSubmit() {
|
|
||||||
this.signing = true;
|
|
||||||
|
|
||||||
(this as any).api('signin', {
|
|
||||||
username: this.username,
|
|
||||||
password: this.password,
|
|
||||||
token: this.user && this.user.twoFactorEnabled ? this.token : undefined
|
|
||||||
}).then(() => {
|
|
||||||
location.reload();
|
|
||||||
}).catch(() => {
|
|
||||||
alert('something happened');
|
|
||||||
this.signing = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.welcome
|
.welcome
|
||||||
background linear-gradient(to bottom, #1e1d65, #bd6659)
|
text-align center
|
||||||
|
//background #fff
|
||||||
|
|
||||||
> div
|
> div
|
||||||
padding 16px
|
padding 32px
|
||||||
margin 0 auto
|
margin 0 auto
|
||||||
max-width 500px
|
max-width 500px
|
||||||
|
|
||||||
h1
|
> img
|
||||||
margin 0
|
display block
|
||||||
padding 8px
|
max-width 200px
|
||||||
font-size 1.5em
|
margin 0 auto
|
||||||
font-weight normal
|
|
||||||
color #cacac3
|
|
||||||
|
|
||||||
& + p
|
> .host
|
||||||
margin 0 0 16px 0
|
display block
|
||||||
padding 0 8px 0 8px
|
text-align center
|
||||||
color #949fa9
|
padding 6px 12px
|
||||||
|
line-height 32px
|
||||||
|
font-weight bold
|
||||||
|
color #333
|
||||||
|
background rgba(#000, 0.035)
|
||||||
|
border-radius 6px
|
||||||
|
|
||||||
.form
|
> .about
|
||||||
margin-bottom 16px
|
margin-top 16px
|
||||||
|
padding 16px
|
||||||
|
color #555
|
||||||
background #fff
|
background #fff
|
||||||
border solid 1px rgba(#000, 0.2)
|
border-radius 6px
|
||||||
border-radius 8px
|
|
||||||
overflow hidden
|
> h2
|
||||||
|
margin 0
|
||||||
|
|
||||||
> p
|
> p
|
||||||
margin 0
|
margin 8px
|
||||||
padding 12px 20px
|
|
||||||
color #555
|
|
||||||
background #f5f5f5
|
|
||||||
border-bottom solid 1px #ddd
|
|
||||||
|
|
||||||
> div
|
> .signup
|
||||||
|
font-weight bold
|
||||||
|
|
||||||
|
> .login
|
||||||
|
margin 16px 0
|
||||||
|
|
||||||
> form
|
> form
|
||||||
padding 16px
|
|
||||||
border-bottom solid 1px #ddd
|
|
||||||
|
|
||||||
input
|
|
||||||
display block
|
|
||||||
padding 12px
|
|
||||||
margin 0 0 16px 0
|
|
||||||
width 100%
|
|
||||||
font-size 1em
|
|
||||||
color rgba(#000, 0.7)
|
|
||||||
background #fff
|
|
||||||
outline none
|
|
||||||
border solid 1px #ddd
|
|
||||||
border-radius 4px
|
|
||||||
|
|
||||||
button
|
button
|
||||||
display block
|
display block
|
||||||
@ -156,40 +115,27 @@ export default Vue.extend({
|
|||||||
border-color #444
|
border-color #444
|
||||||
box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2)
|
box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2)
|
||||||
|
|
||||||
> div
|
|
||||||
padding 16px
|
|
||||||
text-align center
|
|
||||||
|
|
||||||
> .tl
|
> .tl
|
||||||
background #fff
|
> *
|
||||||
border solid 1px rgba(#000, 0.2)
|
|
||||||
border-radius 8px
|
|
||||||
overflow hidden
|
|
||||||
|
|
||||||
> p
|
|
||||||
margin 0
|
|
||||||
padding 12px 20px
|
|
||||||
color #555
|
|
||||||
background #f5f5f5
|
|
||||||
border-bottom solid 1px #ddd
|
|
||||||
|
|
||||||
> .mk-welcome-timeline
|
|
||||||
max-height 300px
|
max-height 300px
|
||||||
|
border-radius 6px
|
||||||
overflow auto
|
overflow auto
|
||||||
|
-webkit-overflow-scrolling touch
|
||||||
|
|
||||||
> .users
|
> .stats
|
||||||
margin 12px 0 0 0
|
margin 16px 0
|
||||||
|
padding 8px
|
||||||
|
font-size 14px
|
||||||
|
color #444
|
||||||
|
background rgba(#000, 0.1)
|
||||||
|
border-radius 6px
|
||||||
|
|
||||||
> *
|
> *
|
||||||
display inline-block
|
margin 0 8px
|
||||||
margin 4px
|
|
||||||
width 38px
|
|
||||||
height 38px
|
|
||||||
border-radius 6px
|
|
||||||
|
|
||||||
> footer
|
> footer
|
||||||
text-align center
|
text-align center
|
||||||
color #fff
|
color #444
|
||||||
|
|
||||||
> small
|
> small
|
||||||
display block
|
display block
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
<option value="rss">%i18n:common.widgets.rss%</option>
|
<option value="rss">%i18n:common.widgets.rss%</option>
|
||||||
<option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
|
<option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
|
||||||
<option value="slideshow">%i18n:common.widgets.slideshow%</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="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
|
||||||
<option value="version">%i18n:common.widgets.version%</option>
|
<option value="version">%i18n:common.widgets.version%</option>
|
||||||
<option value="server">%i18n:common.widgets.server%</option>
|
<option value="server">%i18n:common.widgets.server%</option>
|
||||||
|
@ -56,7 +56,7 @@ export default define({
|
|||||||
left 92px
|
left 92px
|
||||||
margin 0
|
margin 0
|
||||||
line-height 100px
|
line-height 100px
|
||||||
color #fff !important // !important is for md
|
color #fff
|
||||||
font-weight bold
|
font-weight bold
|
||||||
text-shadow 0 0 8px rgba(#000, 0.5)
|
text-shadow 0 0 8px rgba(#000, 0.5)
|
||||||
|
|
||||||
|
BIN
src/client/assets/pointer.png
Normal file
BIN
src/client/assets/pointer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 247 KiB |
@ -1,13 +0,0 @@
|
|||||||
/* SEE: https://vuematerial.io/themes/configuration */
|
|
||||||
|
|
||||||
@import '../const.json';
|
|
||||||
|
|
||||||
@import "~vue-material/dist/theme/engine";
|
|
||||||
|
|
||||||
@include md-register-theme("default", (
|
|
||||||
primary: $themeColor,
|
|
||||||
accent: $themeColor
|
|
||||||
));
|
|
||||||
|
|
||||||
@import "~vue-material/dist/components/MdButton/theme";
|
|
||||||
@import "~vue-material/dist/components/MdField/theme";
|
|
@ -15,6 +15,9 @@ export type Source = {
|
|||||||
*/
|
*/
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
welcome_bg_url?: string;
|
||||||
url: string;
|
url: string;
|
||||||
port: number;
|
port: number;
|
||||||
https?: { [x: string]: string };
|
https?: { [x: string]: string };
|
||||||
|
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;
|
const interval = 5000;
|
||||||
|
|
||||||
setInterval(async () => {
|
async function tick() {
|
||||||
const [all, local] = await Promise.all([Note.count({
|
const [all, local] = await Promise.all([Note.count({
|
||||||
createdAt: {
|
createdAt: {
|
||||||
$gte: new Date(Date.now() - interval)
|
$gte: new Date(Date.now() - interval)
|
||||||
@ -19,4 +19,8 @@ setInterval(async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
process.send(stats);
|
process.send(stats);
|
||||||
}, interval);
|
}
|
||||||
|
|
||||||
|
tick();
|
||||||
|
|
||||||
|
setInterval(tick, interval);
|
@ -5,6 +5,8 @@ import Xev from 'xev';
|
|||||||
|
|
||||||
const ev = new Xev();
|
const ev = new Xev();
|
||||||
|
|
||||||
|
const interval = 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Report server stats regularly
|
* Report server stats regularly
|
||||||
*/
|
*/
|
||||||
@ -15,7 +17,7 @@ export default function() {
|
|||||||
ev.emit('serverStatsLog:' + id, log);
|
ev.emit('serverStatsLog:' + id, log);
|
||||||
});
|
});
|
||||||
|
|
||||||
setInterval(() => {
|
async function tick() {
|
||||||
osUtils.cpuUsage(cpuUsage => {
|
osUtils.cpuUsage(cpuUsage => {
|
||||||
const disk = diskusage.checkSync(os.platform() == 'win32' ? 'c:' : '/');
|
const disk = diskusage.checkSync(os.platform() == 'win32' ? 'c:' : '/');
|
||||||
const stats = {
|
const stats = {
|
||||||
@ -32,5 +34,9 @@ export default function() {
|
|||||||
log.push(stats);
|
log.push(stats);
|
||||||
if (log.length > 50) log.shift();
|
if (log.length > 50) log.shift();
|
||||||
});
|
});
|
||||||
}, 1000);
|
}
|
||||||
|
|
||||||
|
tick();
|
||||||
|
|
||||||
|
setInterval(tick, interval);
|
||||||
}
|
}
|
@ -12,10 +12,7 @@ const uri = u && p
|
|||||||
*/
|
*/
|
||||||
import mongo from 'monk';
|
import mongo from 'monk';
|
||||||
|
|
||||||
const db = mongo(uri, {
|
const db = mongo(uri);
|
||||||
poolSize: 16,
|
|
||||||
keepAlive: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
export default db;
|
export default db;
|
||||||
|
|
||||||
|
@ -17,8 +17,8 @@ import ProgressBar from './utils/cli/progressbar';
|
|||||||
import EnvironmentInfo from './utils/environmentInfo';
|
import EnvironmentInfo from './utils/environmentInfo';
|
||||||
import MachineInfo from './utils/machineInfo';
|
import MachineInfo from './utils/machineInfo';
|
||||||
import DependencyInfo from './utils/dependencyInfo';
|
import DependencyInfo from './utils/dependencyInfo';
|
||||||
import serverStats from './server-stats';
|
import serverStats from './daemons/server-stats';
|
||||||
import notesStats from './notes-stats';
|
import notesStats from './daemons/notes-stats';
|
||||||
|
|
||||||
import loadConfig from './config/load';
|
import loadConfig from './config/load';
|
||||||
import { Config } from './config/types';
|
import { Config } from './config/types';
|
||||||
|
@ -5,4 +5,10 @@ export default Meta;
|
|||||||
|
|
||||||
export type IMeta = {
|
export type IMeta = {
|
||||||
broadcasts: any[];
|
broadcasts: any[];
|
||||||
|
stats: {
|
||||||
|
notesCount: number;
|
||||||
|
originalNotesCount: number;
|
||||||
|
usersCount: number;
|
||||||
|
originalUsersCount: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,7 @@ import Following from './following';
|
|||||||
const Note = db.get<INote>('notes');
|
const Note = db.get<INote>('notes');
|
||||||
Note.createIndex('uri', { sparse: true, unique: true });
|
Note.createIndex('uri', { sparse: true, unique: true });
|
||||||
Note.createIndex('userId');
|
Note.createIndex('userId');
|
||||||
|
Note.createIndex('tagsLower');
|
||||||
Note.createIndex({
|
Note.createIndex({
|
||||||
createdAt: -1
|
createdAt: -1
|
||||||
});
|
});
|
||||||
@ -39,6 +40,7 @@ export type INote = {
|
|||||||
poll: any; // todo
|
poll: any; // todo
|
||||||
text: string;
|
text: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
tagsLower: string[];
|
||||||
cw: string;
|
cw: string;
|
||||||
userId: mongo.ObjectID;
|
userId: mongo.ObjectID;
|
||||||
appId: mongo.ObjectID;
|
appId: mongo.ObjectID;
|
||||||
@ -47,6 +49,11 @@ export type INote = {
|
|||||||
repliesCount: number;
|
repliesCount: number;
|
||||||
reactionCounts: any;
|
reactionCounts: any;
|
||||||
mentions: mongo.ObjectID[];
|
mentions: mongo.ObjectID[];
|
||||||
|
mentionedRemoteUsers: Array<{
|
||||||
|
uri: string;
|
||||||
|
username: string;
|
||||||
|
host: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* public ... 公開
|
* public ... 公開
|
||||||
@ -288,7 +295,7 @@ export const pack = async (
|
|||||||
|
|
||||||
// Poll
|
// Poll
|
||||||
if (meId && _note.poll && !hide) {
|
if (meId && _note.poll && !hide) {
|
||||||
_note.poll = (async (poll) => {
|
_note.poll = (async poll => {
|
||||||
const vote = await PollVote
|
const vote = await PollVote
|
||||||
.findOne({
|
.findOne({
|
||||||
userId: meId,
|
userId: meId,
|
||||||
|
@ -48,6 +48,8 @@ type IUserBase = {
|
|||||||
usernameLower: string;
|
usernameLower: string;
|
||||||
avatarId: mongo.ObjectID;
|
avatarId: mongo.ObjectID;
|
||||||
bannerId: mongo.ObjectID;
|
bannerId: mongo.ObjectID;
|
||||||
|
avatarUrl?: string;
|
||||||
|
bannerUrl?: string;
|
||||||
wallpaperId: mongo.ObjectID;
|
wallpaperId: mongo.ObjectID;
|
||||||
data: any;
|
data: any;
|
||||||
description: string;
|
description: string;
|
||||||
@ -405,13 +407,17 @@ export const pack = (
|
|||||||
delete _user.publicKey;
|
delete _user.publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_user.avatarUrl == null) {
|
||||||
_user.avatarUrl = _user.avatarId != null
|
_user.avatarUrl = _user.avatarId != null
|
||||||
? `${config.drive_url}/${_user.avatarId}`
|
? `${config.drive_url}/${_user.avatarId}`
|
||||||
: `${config.drive_url}/default-avatar.jpg`;
|
: `${config.drive_url}/default-avatar.jpg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_user.bannerUrl == null) {
|
||||||
_user.bannerUrl = _user.bannerId != null
|
_user.bannerUrl = _user.bannerId != null
|
||||||
? `${config.drive_url}/${_user.bannerId}`
|
? `${config.drive_url}/${_user.bannerId}`
|
||||||
: null;
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
_user.wallpaperUrl = _user.wallpaperId != null
|
_user.wallpaperUrl = _user.wallpaperId != null
|
||||||
? `${config.drive_url}/${_user.wallpaperId}`
|
? `${config.drive_url}/${_user.wallpaperId}`
|
||||||
|
@ -15,6 +15,11 @@ const log = debug('misskey:activitypub');
|
|||||||
export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> {
|
export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> {
|
||||||
const uri = activity.id || activity;
|
const uri = activity.id || activity;
|
||||||
|
|
||||||
|
// アナウンサーが凍結されていたらスキップ
|
||||||
|
if (actor.isSuspended) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof uri !== 'string') {
|
if (typeof uri !== 'string') {
|
||||||
throw new Error('invalid announce');
|
throw new Error('invalid announce');
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ import webFinger from '../../webfinger';
|
|||||||
import Resolver from '../resolver';
|
import Resolver from '../resolver';
|
||||||
import { resolveImage } from './image';
|
import { resolveImage } from './image';
|
||||||
import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type';
|
import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type';
|
||||||
|
import { IDriveFile } from '../../../models/drive-file';
|
||||||
|
import Meta from '../../../models/meta';
|
||||||
|
|
||||||
const log = debug('misskey:activitypub');
|
const log = debug('misskey:activitypub');
|
||||||
|
|
||||||
@ -116,20 +118,42 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#region Increment users count
|
||||||
|
Meta.update({}, {
|
||||||
|
$inc: {
|
||||||
|
'stats.usersCount': 1
|
||||||
|
}
|
||||||
|
}, { upsert: true });
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region アイコンとヘッダー画像をフェッチ
|
//#region アイコンとヘッダー画像をフェッチ
|
||||||
const [avatarId, bannerId] = (await Promise.all([
|
const [avatar, banner] = (await Promise.all<IDriveFile>([
|
||||||
person.icon,
|
person.icon,
|
||||||
person.image
|
person.image
|
||||||
].map(img =>
|
].map(img =>
|
||||||
img == null
|
img == null
|
||||||
? Promise.resolve(null)
|
? Promise.resolve(null)
|
||||||
: resolveImage(user, img)
|
: 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.avatarId = avatarId;
|
||||||
user.bannerId = bannerId;
|
user.bannerId = bannerId;
|
||||||
|
user.avatarUrl = avatarUrl;
|
||||||
|
user.bannerUrl = bannerUrl;
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
@ -190,21 +214,23 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
|
|||||||
const summaryDOM = JSDOM.fragment(person.summary);
|
const summaryDOM = JSDOM.fragment(person.summary);
|
||||||
|
|
||||||
// アイコンとヘッダー画像をフェッチ
|
// アイコンとヘッダー画像をフェッチ
|
||||||
const [avatarId, bannerId] = (await Promise.all([
|
const [avatar, banner] = (await Promise.all<IDriveFile>([
|
||||||
person.icon,
|
person.icon,
|
||||||
person.image
|
person.image
|
||||||
].map(img =>
|
].map(img =>
|
||||||
img == null
|
img == null
|
||||||
? Promise.resolve(null)
|
? Promise.resolve(null)
|
||||||
: resolveImage(exist, img)
|
: resolveImage(exist, img)
|
||||||
))).map(file => file != null ? file._id : null);
|
)));
|
||||||
|
|
||||||
// Update user
|
// Update user
|
||||||
await User.update({ _id: exist._id }, {
|
await User.update({ _id: exist._id }, {
|
||||||
$set: {
|
$set: {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
avatarId,
|
avatarId: avatar ? avatar._id : null,
|
||||||
bannerId,
|
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,
|
description: summaryDOM.textContent,
|
||||||
followersCount,
|
followersCount,
|
||||||
followingCount,
|
followingCount,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
|
|
||||||
export default tag => ({
|
export default (tag: string) => ({
|
||||||
type: 'Hashtag',
|
type: 'Hashtag',
|
||||||
href: `${config.url}/search?q=#${encodeURIComponent(tag)}`,
|
href: `${config.url}/tags/${encodeURIComponent(tag)}`,
|
||||||
name: '#' + tag
|
name: '#' + tag
|
||||||
});
|
});
|
||||||
|
9
src/remote/activitypub/renderer/mention.ts
Normal file
9
src/remote/activitypub/renderer/mention.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default (mention: {
|
||||||
|
uri: string;
|
||||||
|
username: string;
|
||||||
|
host: string;
|
||||||
|
}) => ({
|
||||||
|
type: 'Mention',
|
||||||
|
href: mention.uri,
|
||||||
|
name: `@${mention.username}@${mention.host}`
|
||||||
|
});
|
@ -1,5 +1,6 @@
|
|||||||
import renderDocument from './document';
|
import renderDocument from './document';
|
||||||
import renderHashtag from './hashtag';
|
import renderHashtag from './hashtag';
|
||||||
|
import renderMention from './mention';
|
||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
import DriveFile from '../../../models/drive-file';
|
import DriveFile from '../../../models/drive-file';
|
||||||
import Note, { INote } from '../../../models/note';
|
import Note, { INote } from '../../../models/note';
|
||||||
@ -45,6 +46,18 @@ export default async function renderNote(note: INote, dive = true) {
|
|||||||
|
|
||||||
const attributedTo = `${config.url}/users/${user._id}`;
|
const attributedTo = `${config.url}/users/${user._id}`;
|
||||||
|
|
||||||
|
const mentions = note.mentionedRemoteUsers && note.mentionedRemoteUsers.length > 0
|
||||||
|
? note.mentionedRemoteUsers.map(x => x.uri)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const cc = ['public', 'home', 'followers'].includes(note.visibility)
|
||||||
|
? [`${attributedTo}/followers`].concat(mentions)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const hashtagTags = (note.tags || []).map(renderHashtag);
|
||||||
|
const mentionTags = (note.mentionedRemoteUsers || []).map(renderMention);
|
||||||
|
const tag = hashtagTags.concat(mentionTags)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${config.url}/notes/${note._id}`,
|
id: `${config.url}/notes/${note._id}`,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
@ -52,9 +65,9 @@ export default async function renderNote(note: INote, dive = true) {
|
|||||||
content: toHtml(note),
|
content: toHtml(note),
|
||||||
published: note.createdAt.toISOString(),
|
published: note.createdAt.toISOString(),
|
||||||
to: 'https://www.w3.org/ns/activitystreams#Public',
|
to: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
cc: `${attributedTo}/followers`,
|
cc,
|
||||||
inReplyTo,
|
inReplyTo,
|
||||||
attachment: (await promisedFiles).map(renderDocument),
|
attachment: (await promisedFiles).map(renderDocument),
|
||||||
tag: (note.tags || []).map(renderHashtag)
|
tag
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -525,6 +525,9 @@ const endpoints: Endpoint[] = [
|
|||||||
{
|
{
|
||||||
name: 'notes/search'
|
name: 'notes/search'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'notes/search_by_tag'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'notes/timeline',
|
name: 'notes/timeline',
|
||||||
withCredential: true,
|
withCredential: true,
|
||||||
@ -625,6 +628,11 @@ const endpoints: Endpoint[] = [
|
|||||||
withCredential: true
|
withCredential: true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'hashtags/trend',
|
||||||
|
withCredential: true
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'messaging/history',
|
name: 'messaging/history',
|
||||||
withCredential: true,
|
withCredential: true,
|
||||||
|
@ -1,34 +1,32 @@
|
|||||||
/**
|
|
||||||
* Module dependencies
|
|
||||||
*/
|
|
||||||
import DriveFile from '../../../models/drive-file';
|
import DriveFile from '../../../models/drive-file';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get drive information
|
* Get drive information
|
||||||
*
|
|
||||||
* @param {any} params
|
|
||||||
* @param {any} user
|
|
||||||
* @return {Promise<any>}
|
|
||||||
*/
|
*/
|
||||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
// Calculate drive usage
|
// Calculate drive usage
|
||||||
const usage = ((await DriveFile
|
const usage = await DriveFile
|
||||||
.aggregate([
|
.aggregate([{
|
||||||
{ $match: { 'metadata.userId': user._id } },
|
$match: {
|
||||||
{
|
'metadata.userId': user._id,
|
||||||
|
'metadata.deletedAt': { $exists: false }
|
||||||
|
}
|
||||||
|
}, {
|
||||||
$project: {
|
$project: {
|
||||||
length: true
|
length: true
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
$group: {
|
$group: {
|
||||||
_id: null,
|
_id: null,
|
||||||
usage: { $sum: '$length' }
|
usage: { $sum: '$length' }
|
||||||
}
|
}
|
||||||
|
}])
|
||||||
|
.then((aggregates: any[]) => {
|
||||||
|
if (aggregates.length > 0) {
|
||||||
|
return aggregates[0].usage;
|
||||||
}
|
}
|
||||||
]))[0] || {
|
return 0;
|
||||||
usage: 0
|
});
|
||||||
}).usage;
|
|
||||||
|
|
||||||
res({
|
res({
|
||||||
capacity: user.driveCapacity,
|
capacity: user.driveCapacity,
|
||||||
|
@ -37,10 +37,13 @@ module.exports = async (params, user, app) => {
|
|||||||
const sort = {
|
const sort = {
|
||||||
_id: -1
|
_id: -1
|
||||||
};
|
};
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
'metadata.userId': user._id,
|
'metadata.userId': user._id,
|
||||||
'metadata.folderId': folderId
|
'metadata.folderId': folderId,
|
||||||
|
'metadata.deletedAt': { $exists: false }
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
if (sinceId) {
|
if (sinceId) {
|
||||||
sort._id = 1;
|
sort._id = 1;
|
||||||
query._id = {
|
query._id = {
|
||||||
@ -51,6 +54,7 @@ module.exports = async (params, user, app) => {
|
|||||||
$lt: untilId
|
$lt: untilId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type) {
|
if (type) {
|
||||||
query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
|
query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
|
||||||
}
|
}
|
||||||
|
32
src/server/api/endpoints/drive/files/delete.ts
Normal file
32
src/server/api/endpoints/drive/files/delete.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import $ from 'cafy'; import ID from '../../../../../cafy-id';
|
||||||
|
import DriveFile from '../../../../../models/drive-file';
|
||||||
|
import del from '../../../../../services/drive/delete-file';
|
||||||
|
import { publishDriveStream } from '../../../../../publishers/stream';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file
|
||||||
|
*/
|
||||||
|
module.exports = async (params, user) => {
|
||||||
|
// Get 'fileId' parameter
|
||||||
|
const [fileId, fileIdErr] = $.type(ID).get(params.fileId);
|
||||||
|
if (fileIdErr) throw 'invalid fileId param';
|
||||||
|
|
||||||
|
// Fetch file
|
||||||
|
const file = await DriveFile
|
||||||
|
.findOne({
|
||||||
|
_id: fileId,
|
||||||
|
'metadata.userId': user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (file === null) {
|
||||||
|
throw 'file-not-found';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
await del(file);
|
||||||
|
|
||||||
|
// Publish file_deleted event
|
||||||
|
publishDriveStream(user._id, 'file_deleted', file._id);
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
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.25; // 「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)
|
||||||
|
},
|
||||||
|
tagsLower: {
|
||||||
|
$exists: true,
|
||||||
|
$ne: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
$unwind: '$tagsLower'
|
||||||
|
}, {
|
||||||
|
$group: {
|
||||||
|
_id: { tag: '$tagsLower', userId: '$userId' }
|
||||||
|
}
|
||||||
|
}]) as Array<{
|
||||||
|
_id: {
|
||||||
|
tag: 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.tag);
|
||||||
|
if (i != -1) {
|
||||||
|
tags[i].count++;
|
||||||
|
} else {
|
||||||
|
tags.push({
|
||||||
|
name: x.tag,
|
||||||
|
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', {
|
||||||
|
tagsLower: 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', {
|
||||||
|
tagsLower: 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', {
|
||||||
|
tagsLower: 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: [{
|
||||||
|
tagsLower: tag.toLowerCase()
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
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))));
|
||||||
|
}
|
@ -1,26 +1,10 @@
|
|||||||
import Note from '../../../models/note';
|
import Meta from '../../../models/meta';
|
||||||
import User from '../../../models/user';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the misskey's statistics
|
* Get the misskey's statistics
|
||||||
*/
|
*/
|
||||||
module.exports = params => new Promise(async (res, rej) => {
|
module.exports = () => new Promise(async (res, rej) => {
|
||||||
const notesCount = await Note.count();
|
const meta = await Meta.findOne();
|
||||||
|
|
||||||
const usersCount = await User.count();
|
res(meta.stats);
|
||||||
|
|
||||||
const originalNotesCount = await Note.count({
|
|
||||||
'_user.host': null
|
|
||||||
});
|
|
||||||
|
|
||||||
const originalUsersCount = await User.count({
|
|
||||||
host: null
|
|
||||||
});
|
|
||||||
|
|
||||||
res({
|
|
||||||
notesCount,
|
|
||||||
usersCount,
|
|
||||||
originalNotesCount,
|
|
||||||
originalUsersCount
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -5,6 +5,7 @@ import recaptcha = require('recaptcha-promise');
|
|||||||
import User, { IUser, validateUsername, validatePassword, pack } from '../../../models/user';
|
import User, { IUser, validateUsername, validatePassword, pack } from '../../../models/user';
|
||||||
import generateUserToken from '../common/generate-native-user-token';
|
import generateUserToken from '../common/generate-native-user-token';
|
||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
|
import Meta from '../../../models/meta';
|
||||||
|
|
||||||
recaptcha.init({
|
recaptcha.init({
|
||||||
secret_key: config.recaptcha.secret_key
|
secret_key: config.recaptcha.secret_key
|
||||||
@ -93,6 +94,15 @@ export default async (ctx: Koa.Context) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//#region Increment users count
|
||||||
|
Meta.update({}, {
|
||||||
|
$inc: {
|
||||||
|
'stats.usersCount': 1,
|
||||||
|
'stats.originalUsersCount': 1
|
||||||
|
}
|
||||||
|
}, { upsert: true });
|
||||||
|
//#endregion
|
||||||
|
|
||||||
// Response
|
// Response
|
||||||
ctx.body = await pack(account);
|
ctx.body = await pack(account);
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import * as debug from 'debug';
|
|||||||
|
|
||||||
import User, { IUser } from '../../../models/user';
|
import User, { IUser } from '../../../models/user';
|
||||||
import Mute from '../../../models/mute';
|
import Mute from '../../../models/mute';
|
||||||
import { pack as packNote } from '../../../models/note';
|
import { pack as packNote, pack } from '../../../models/note';
|
||||||
import readNotification from '../common/read-notification';
|
import readNotification from '../common/read-notification';
|
||||||
import call from '../call';
|
import call from '../call';
|
||||||
import { IApp } from '../../../models/app';
|
import { IApp } from '../../../models/app';
|
||||||
@ -48,6 +48,14 @@ export default async function(
|
|||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
// Renoteなら再pack
|
||||||
|
if (x.type == 'note' && x.body.renoteId != null) {
|
||||||
|
x.body.renote = await pack(x.body.renoteId, user, {
|
||||||
|
detail: true
|
||||||
|
});
|
||||||
|
data = JSON.stringify(x);
|
||||||
|
}
|
||||||
|
|
||||||
connection.send(data);
|
connection.send(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
connection.send(data);
|
connection.send(data);
|
||||||
|
@ -3,6 +3,7 @@ import * as redis from 'redis';
|
|||||||
|
|
||||||
import { IUser } from '../../../models/user';
|
import { IUser } from '../../../models/user';
|
||||||
import Mute from '../../../models/mute';
|
import Mute from '../../../models/mute';
|
||||||
|
import { pack } from '../../../models/note';
|
||||||
|
|
||||||
export default async function(
|
export default async function(
|
||||||
request: websocket.request,
|
request: websocket.request,
|
||||||
@ -31,6 +32,13 @@ export default async function(
|
|||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
// Renoteなら再pack
|
||||||
|
if (note.renoteId != null) {
|
||||||
|
note.renote = await pack(note.renoteId, user, {
|
||||||
|
detail: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
connection.send(JSON.stringify({
|
connection.send(JSON.stringify({
|
||||||
type: 'note',
|
type: 'note',
|
||||||
body: note
|
body: note
|
||||||
|
@ -9,13 +9,14 @@ import * as debug from 'debug';
|
|||||||
import fileType = require('file-type');
|
import fileType = require('file-type');
|
||||||
import prominence = require('prominence');
|
import prominence = require('prominence');
|
||||||
|
|
||||||
import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile, DriveFileChunk } from '../../models/drive-file';
|
import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../models/drive-file';
|
||||||
import DriveFolder from '../../models/drive-folder';
|
import DriveFolder from '../../models/drive-folder';
|
||||||
import { pack } from '../../models/drive-file';
|
import { pack } from '../../models/drive-file';
|
||||||
import event, { publishDriveStream } from '../../publishers/stream';
|
import event, { publishDriveStream } from '../../publishers/stream';
|
||||||
import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
|
import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
|
||||||
import DriveFileThumbnail, { getDriveFileThumbnailBucket, DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
|
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
|
||||||
import genThumbnail from '../../drive/gen-thumbnail';
|
import genThumbnail from '../../drive/gen-thumbnail';
|
||||||
|
import delFile from './delete-file';
|
||||||
|
|
||||||
const gm = _gm.subClass({
|
const gm = _gm.subClass({
|
||||||
imageMagick: true
|
imageMagick: true
|
||||||
@ -58,31 +59,7 @@ async function deleteOldFile(user: IRemoteUser) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (oldFile) {
|
if (oldFile) {
|
||||||
// チャンクをすべて削除
|
delFile(oldFile, true);
|
||||||
DriveFileChunk.remove({
|
|
||||||
files_id: oldFile._id
|
|
||||||
});
|
|
||||||
|
|
||||||
DriveFile.update({ _id: oldFile._id }, {
|
|
||||||
$set: {
|
|
||||||
'metadata.deletedAt': new Date(),
|
|
||||||
'metadata.isExpired': true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//#region サムネイルもあれば削除
|
|
||||||
const thumbnail = await DriveFileThumbnail.findOne({
|
|
||||||
'metadata.originalId': oldFile._id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (thumbnail) {
|
|
||||||
DriveFileThumbnailChunk.remove({
|
|
||||||
files_id: thumbnail._id
|
|
||||||
});
|
|
||||||
|
|
||||||
DriveFileThumbnail.remove({ _id: thumbnail._id });
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
30
src/services/drive/delete-file.ts
Normal file
30
src/services/drive/delete-file.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import DriveFile, { DriveFileChunk, IDriveFile } from "../../models/drive-file";
|
||||||
|
import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
|
||||||
|
|
||||||
|
export default async function(file: IDriveFile, isExpired = false) {
|
||||||
|
// チャンクをすべて削除
|
||||||
|
await DriveFileChunk.remove({
|
||||||
|
files_id: file._id
|
||||||
|
});
|
||||||
|
|
||||||
|
await DriveFile.update({ _id: file._id }, {
|
||||||
|
$set: {
|
||||||
|
'metadata.deletedAt': new Date(),
|
||||||
|
'metadata.isExpired': isExpired
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//#region サムネイルもあれば削除
|
||||||
|
const thumbnail = await DriveFileThumbnail.findOne({
|
||||||
|
'metadata.originalId': file._id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (thumbnail) {
|
||||||
|
await DriveFileThumbnailChunk.remove({
|
||||||
|
files_id: thumbnail._id
|
||||||
|
});
|
||||||
|
|
||||||
|
await DriveFileThumbnail.remove({ _id: thumbnail._id });
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
}
|
@ -18,6 +18,7 @@ import parse from '../../text/parse';
|
|||||||
import { IApp } from '../../models/app';
|
import { IApp } from '../../models/app';
|
||||||
import UserList from '../../models/user-list';
|
import UserList from '../../models/user-list';
|
||||||
import resolveUser from '../../remote/resolve-user';
|
import resolveUser from '../../remote/resolve-user';
|
||||||
|
import Meta from '../../models/meta';
|
||||||
|
|
||||||
type Reason = 'reply' | 'quote' | 'mention';
|
type Reason = 'reply' | 'quote' | 'mention';
|
||||||
|
|
||||||
@ -94,7 +95,7 @@ export default async (user: IUser, data: {
|
|||||||
if (data.visibility == null) data.visibility = 'public';
|
if (data.visibility == null) data.visibility = 'public';
|
||||||
if (data.viaMobile == null) data.viaMobile = false;
|
if (data.viaMobile == null) data.viaMobile = false;
|
||||||
|
|
||||||
const tags = data.tags || [];
|
let tags = data.tags || [];
|
||||||
|
|
||||||
let tokens: any[] = null;
|
let tokens: any[] = null;
|
||||||
|
|
||||||
@ -114,6 +115,8 @@ export default async (user: IUser, data: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tags = tags.filter(tag => tag.length <= 100);
|
||||||
|
|
||||||
if (data.visibleUsers) {
|
if (data.visibleUsers) {
|
||||||
data.visibleUsers = data.visibleUsers.filter(x => x != null);
|
data.visibleUsers = data.visibleUsers.filter(x => x != null);
|
||||||
}
|
}
|
||||||
@ -127,6 +130,7 @@ export default async (user: IUser, data: {
|
|||||||
poll: data.poll,
|
poll: data.poll,
|
||||||
cw: data.cw == null ? null : data.cw,
|
cw: data.cw == null ? null : data.cw,
|
||||||
tags,
|
tags,
|
||||||
|
tagsLower: tags.map(tag => tag.toLowerCase()),
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
viaMobile: data.viaMobile,
|
viaMobile: data.viaMobile,
|
||||||
geo: data.geo || null,
|
geo: data.geo || null,
|
||||||
@ -165,7 +169,24 @@ export default async (user: IUser, data: {
|
|||||||
|
|
||||||
res(note);
|
res(note);
|
||||||
|
|
||||||
// Increment notes count
|
//#region Increment notes count
|
||||||
|
if (isLocalUser(user)) {
|
||||||
|
Meta.update({}, {
|
||||||
|
$inc: {
|
||||||
|
'stats.notesCount': 1,
|
||||||
|
'stats.originalNotesCount': 1
|
||||||
|
}
|
||||||
|
}, { upsert: true });
|
||||||
|
} else {
|
||||||
|
Meta.update({}, {
|
||||||
|
$inc: {
|
||||||
|
'stats.notesCount': 1
|
||||||
|
}
|
||||||
|
}, { upsert: true });
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
// Increment notes count (user)
|
||||||
User.update({ _id: user._id }, {
|
User.update({ _id: user._id }, {
|
||||||
$inc: {
|
$inc: {
|
||||||
notesCount: 1
|
notesCount: 1
|
||||||
@ -202,6 +223,62 @@ export default async (user: IUser, data: {
|
|||||||
return packAp(content);
|
return packAp(content);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//#region メンション
|
||||||
|
if (data.text) {
|
||||||
|
// TODO: Drop dupulicates
|
||||||
|
const mentionTokens = tokens
|
||||||
|
.filter(t => t.type == 'mention');
|
||||||
|
|
||||||
|
// TODO: Drop dupulicates
|
||||||
|
const mentionedUsers = (await Promise.all(mentionTokens.map(async m => {
|
||||||
|
try {
|
||||||
|
return await resolveUser(m.username, m.host);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}))).filter(x => x != null);
|
||||||
|
|
||||||
|
// Append mentions data
|
||||||
|
if (mentionedUsers.length > 0) {
|
||||||
|
const set = {
|
||||||
|
mentions: mentionedUsers.map(u => u._id),
|
||||||
|
mentionedRemoteUsers: mentionedUsers.filter(u => isRemoteUser(u)).map(u => ({
|
||||||
|
uri: (u as IRemoteUser).uri,
|
||||||
|
username: u.username,
|
||||||
|
host: u.host
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
Note.update({ _id: note._id }, {
|
||||||
|
$set: set
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(note, set);
|
||||||
|
}
|
||||||
|
|
||||||
|
mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => {
|
||||||
|
event(u, 'mention', noteObj);
|
||||||
|
|
||||||
|
// 既に言及されたユーザーに対する返信や引用renoteの場合も無視
|
||||||
|
if (data.reply && data.reply.userId.equals(u._id)) return;
|
||||||
|
if (data.renote && data.renote.userId.equals(u._id)) return;
|
||||||
|
|
||||||
|
// Create notification
|
||||||
|
notify(u._id, user._id, 'mention', {
|
||||||
|
noteId: note._id
|
||||||
|
});
|
||||||
|
|
||||||
|
nm.push(u._id, 'mention');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLocalUser(user)) {
|
||||||
|
mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => {
|
||||||
|
deliver(user, await render(), (u as IRemoteUser).inbox);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
if (isLocalUser(user)) {
|
if (isLocalUser(user)) {
|
||||||
if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') {
|
if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') {
|
||||||
@ -285,55 +362,6 @@ export default async (user: IUser, data: {
|
|||||||
}
|
}
|
||||||
//#endergion
|
//#endergion
|
||||||
|
|
||||||
//#region メンション
|
|
||||||
if (data.text) {
|
|
||||||
// TODO: Drop dupulicates
|
|
||||||
const mentions = tokens
|
|
||||||
.filter(t => t.type == 'mention');
|
|
||||||
|
|
||||||
let mentionedUsers = await Promise.all(mentions.map(async m => {
|
|
||||||
try {
|
|
||||||
return await resolveUser(m.username, m.host);
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// TODO: Drop dupulicates
|
|
||||||
mentionedUsers = mentionedUsers.filter(x => x != null);
|
|
||||||
|
|
||||||
mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => {
|
|
||||||
event(u, 'mention', noteObj);
|
|
||||||
|
|
||||||
// 既に言及されたユーザーに対する返信や引用renoteの場合も無視
|
|
||||||
if (data.reply && data.reply.userId.equals(u._id)) return;
|
|
||||||
if (data.renote && data.renote.userId.equals(u._id)) return;
|
|
||||||
|
|
||||||
// Create notification
|
|
||||||
notify(u._id, user._id, 'mention', {
|
|
||||||
noteId: note._id
|
|
||||||
});
|
|
||||||
|
|
||||||
nm.push(u._id, 'mention');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLocalUser(user)) {
|
|
||||||
mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => {
|
|
||||||
deliver(user, await render(), (u as IRemoteUser).inbox);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append mentions data
|
|
||||||
if (mentionedUsers.length > 0) {
|
|
||||||
Note.update({ _id: note._id }, {
|
|
||||||
$set: {
|
|
||||||
mentions: mentionedUsers.map(u => u._id)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
// If has in reply to note
|
// If has in reply to note
|
||||||
if (data.reply) {
|
if (data.reply) {
|
||||||
// Increment replies count
|
// Increment replies count
|
||||||
|
@ -20,6 +20,7 @@ export default async function(user: IUser, note: INote) {
|
|||||||
$set: {
|
$set: {
|
||||||
deletedAt: new Date(),
|
deletedAt: new Date(),
|
||||||
text: null,
|
text: null,
|
||||||
|
tags: [],
|
||||||
mediaIds: [],
|
mediaIds: [],
|
||||||
poll: null
|
poll: null
|
||||||
}
|
}
|
||||||
|
@ -25,8 +25,10 @@ const handlers = {
|
|||||||
|
|
||||||
hashtag({ document }, { hashtag }) {
|
hashtag({ document }, { hashtag }) {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = '/search?q=#' + hashtag;
|
a.href = config.url + '/tags/' + hashtag;
|
||||||
a.textContent = hashtag;
|
a.textContent = '#' + hashtag;
|
||||||
|
a.setAttribute('rel', 'tag');
|
||||||
|
document.body.appendChild(a);
|
||||||
},
|
},
|
||||||
|
|
||||||
'inline-code'({ document }, { code }) {
|
'inline-code'({ document }, { code }) {
|
||||||
|
@ -79,11 +79,14 @@ const consts = {
|
|||||||
_DEV_URL_: config.dev_url,
|
_DEV_URL_: config.dev_url,
|
||||||
_LANG_: '%lang%',
|
_LANG_: '%lang%',
|
||||||
_LANGS_: Object.keys(locales).map(l => [l, locales[l].meta.lang]),
|
_LANGS_: Object.keys(locales).map(l => [l, locales[l].meta.lang]),
|
||||||
|
_NAME_: config.name,
|
||||||
|
_DESCRIPTION_: config.description,
|
||||||
_HOST_: config.host,
|
_HOST_: config.host,
|
||||||
_HOSTNAME_: config.hostname,
|
_HOSTNAME_: config.hostname,
|
||||||
_URL_: config.url,
|
_URL_: config.url,
|
||||||
_LICENSE_: licenseHtml,
|
_LICENSE_: licenseHtml,
|
||||||
_GOOGLE_MAPS_API_KEY_: config.google_maps_api_key
|
_GOOGLE_MAPS_API_KEY_: config.google_maps_api_key,
|
||||||
|
_WELCOME_BG_URL_: config.welcome_bg_url
|
||||||
};
|
};
|
||||||
|
|
||||||
const _consts = {};
|
const _consts = {};
|
||||||
|
Reference in New Issue
Block a user