Compare commits
107 Commits
Author | SHA1 | Date | |
---|---|---|---|
e3faf64061 | |||
ed83993e15 | |||
0f8847bb74 | |||
a72cfa7535 | |||
514b74a19d | |||
a2c124306f | |||
273f67e268 | |||
2870a7e463 | |||
65e5cfa68e | |||
10e59957d1 | |||
4f74373df3 | |||
2d414bbf86 | |||
a199969b81 | |||
3aef5e6748 | |||
2b536a7443 | |||
20fe68de05 | |||
c7684b59de | |||
a7237d157a | |||
35f91fa280 | |||
299ac32225 | |||
a038738d72 | |||
2b0a919fb5 | |||
946c706913 | |||
89b5d976ee | |||
6f679bb6b4 | |||
db4e7b0e16 | |||
9ca942490d | |||
ebcf249c8b | |||
939c487503 | |||
981a8b267e | |||
9531da80a0 | |||
e1109b168c | |||
b7c70039aa | |||
17b6f6cf2a | |||
dd88483ba4 | |||
0ff27f65b3 | |||
b1655740df | |||
6d562aece1 | |||
2182c3372b | |||
d3331bfe82 | |||
cfc4a2e8b4 | |||
36c41c8eb3 | |||
d255157e6e | |||
c12e07277d | |||
06b4fb5095 | |||
8fafdcb428 | |||
537a606bb6 | |||
3dc7a4463c | |||
fd6ff05b60 | |||
1a159e41b8 | |||
23533cdd16 | |||
2f598b8fa1 | |||
bca349fec1 | |||
719fac6480 | |||
1012b2b2c7 | |||
5149be4b1b | |||
d12deeb0d8 | |||
9df81d1939 | |||
3be0079868 | |||
9b253ccb3a | |||
dded76099c | |||
41a7ec7d3d | |||
168c773ba0 | |||
9abed92196 | |||
4a75e3602a | |||
1a689f6641 | |||
08d7ae11d6 | |||
9535759787 | |||
f8fc31f14a | |||
b74bf97761 | |||
a090b908bd | |||
3046821026 | |||
e94c73efe2 | |||
e85f9f4aa5 | |||
ad67886f96 | |||
5df0e102fd | |||
a04f0e3545 | |||
dff9c7ac48 | |||
3a80b59986 | |||
07560a4fdd | |||
7edca21c05 | |||
34105abd9d | |||
1bbca48a0b | |||
21f6a86772 | |||
6559197c55 | |||
05f9ad11bb | |||
f06d586680 | |||
4f45e8125c | |||
cc2843503d | |||
324a974dec | |||
4d4ffd70ac | |||
bf98a11b65 | |||
1117ce4b54 | |||
57e93b9b4e | |||
9e4b061ed0 | |||
1067bef7d6 | |||
8bff529acd | |||
4b08677839 | |||
70997cb551 | |||
bf0ef17e23 | |||
7dae5107f8 | |||
2dea88a147 | |||
b9290a021b | |||
129ce93868 | |||
5f41e5d6d0 | |||
c706d030ea | |||
34716a34f8 |
@ -60,11 +60,6 @@ mongodb:
|
||||
user: example-misskey-user
|
||||
pass: example-misskey-pass
|
||||
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
pass: example-pass
|
||||
|
||||
# Drive capacity of a local user (MB)
|
||||
localDriveCapacityMb: 256
|
||||
|
||||
@ -122,47 +117,50 @@ drive:
|
||||
# Below settings are optional
|
||||
#
|
||||
|
||||
# Redis
|
||||
#redis:
|
||||
# host: localhost
|
||||
# port: 6379
|
||||
# pass: example-pass
|
||||
|
||||
# Elasticsearch
|
||||
# elasticsearch:
|
||||
# host: localhost
|
||||
# port: 9200
|
||||
# pass: null
|
||||
#elasticsearch:
|
||||
# host: localhost
|
||||
# port: 9200
|
||||
# pass: null
|
||||
|
||||
# reCAPTCHA
|
||||
# recaptcha:
|
||||
# site_key: example-site-key
|
||||
#recaptcha:
|
||||
# site_key: example-site-key
|
||||
# secret_key: example-secret-key
|
||||
|
||||
# ServiceWorker
|
||||
# sw:
|
||||
# # Public key of VAPID
|
||||
# public_key: example-sw-public-key
|
||||
|
||||
# # Private key of VAPID
|
||||
# private_key: example-sw-private-key
|
||||
|
||||
# google_maps_api_key: example-google-maps-api-key
|
||||
#sw:
|
||||
# # Public key of VAPID
|
||||
# public_key: example-sw-public-key
|
||||
#
|
||||
# # Private key of VAPID
|
||||
# private_key: example-sw-private-key
|
||||
|
||||
# Twitter integration
|
||||
# You need to set the oauth callback url as : https://<your-misskey-instance>/api/tw/cb
|
||||
# twitter:
|
||||
# consumer_key: example-twitter-consumer-key
|
||||
# consumer_secret: example-twitter-consumer-secret-key
|
||||
#twitter:
|
||||
# consumer_key: example-twitter-consumer-key
|
||||
# consumer_secret: example-twitter-consumer-secret-key
|
||||
|
||||
# Ghost
|
||||
# Ghost account is an account used for the purpose of delegating
|
||||
# followers when putting users in the list.
|
||||
# ghost: user-id-of-your-ghost-account
|
||||
#ghost: user-id-of-your-ghost-account
|
||||
|
||||
# Clustering
|
||||
# clusterLimit: 1
|
||||
#clusterLimit: 1
|
||||
|
||||
# Summaly proxy
|
||||
# summalyProxy: "http://example.com"
|
||||
#summalyProxy: "http://example.com"
|
||||
|
||||
# User recommendation
|
||||
user_recommendation:
|
||||
external: true
|
||||
engine: http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}
|
||||
timeout: 300000
|
||||
|
||||
#user_recommendation:
|
||||
# external: true
|
||||
# engine: http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}
|
||||
# timeout: 300000
|
||||
|
@ -24,12 +24,12 @@ Please install and setup these softwares:
|
||||
#### Dependencies :package:
|
||||
* **[Node.js](https://nodejs.org/en/)**
|
||||
* **[MongoDB](https://www.mongodb.com/)** >= 3.6
|
||||
* **[Redis](https://redis.io/)**
|
||||
|
||||
##### Optional
|
||||
* [Redis](https://redis.io/)
|
||||
* Redis is optional, but we strongly recommended to install it
|
||||
* [Elasticsearch](https://www.elastic.co/) - used to provide searching feature instead of MongoDB
|
||||
|
||||
|
||||
*3.* Setup MongoDB
|
||||
----------------------------------------------------------------
|
||||
In root :
|
||||
|
@ -24,10 +24,17 @@ adduser --disabled-password --disabled-login misskey
|
||||
#### 依存関係 :package:
|
||||
* **[Node.js](https://nodejs.org/en/)**
|
||||
* **[MongoDB](https://www.mongodb.com/)** (3.6以上)
|
||||
* **[Redis](https://redis.io/)**
|
||||
|
||||
##### オプション
|
||||
* [Elasticsearch](https://www.elastic.co/) - 検索機能を向上させるために用います。
|
||||
* [Redis](https://redis.io/)
|
||||
* Redisはオプションですが、インストールすることを強く推奨します。
|
||||
* インストールしなくていいのは、あなたのインスタンスが自分専用のときだけとお考えください。
|
||||
* 具体的には、Redisをインストールしないと、次の事が出来なくなります:
|
||||
* Misskeyプロセスを複数起動しての負荷分散
|
||||
* レートリミット
|
||||
* Twitter連携
|
||||
* [Elasticsearch](https://www.elastic.co/)
|
||||
* 検索機能を有効にするためにはインストールが必要です。
|
||||
|
||||
*3.* MongoDBの設定
|
||||
----------------------------------------------------------------
|
||||
|
@ -335,6 +335,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "詳細"
|
||||
copy-link: "リンクをコピー"
|
||||
favorite: "お気に入り"
|
||||
unfavorite: "お気に入り解除"
|
||||
pin: "ピン留め"
|
||||
unpin: "ピン留め解除"
|
||||
delete: "削除"
|
||||
|
@ -335,6 +335,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "詳細"
|
||||
copy-link: "リンクをコピー"
|
||||
favorite: "Diese Anmerkung favorisieren"
|
||||
unfavorite: "お気に入り解除"
|
||||
pin: "An die Profilseite pinnen"
|
||||
unpin: "ピン留め解除"
|
||||
delete: "Löschen"
|
||||
|
@ -265,40 +265,40 @@ common/views/components/media-banner.vue:
|
||||
sensitive: "NSFW"
|
||||
click-to-show: "Click to show"
|
||||
common/views/components/theme.vue:
|
||||
light-theme: "非ダークモード時に使用するテーマ"
|
||||
dark-theme: "ダークモード時に使用するテーマ"
|
||||
light-themes: "明るいテーマ"
|
||||
dark-themes: "暗いテーマ"
|
||||
install-a-theme: "テーマのインストール"
|
||||
theme-code: "テーマコード"
|
||||
install: "インストール"
|
||||
installed: "「{}」をインストールしました"
|
||||
create-a-theme: "テーマの作成"
|
||||
save-created-theme: "テーマを保存"
|
||||
primary-color: "プライマリ カラー"
|
||||
secondary-color: "セカンダリ カラー"
|
||||
text-color: "文字色"
|
||||
base-theme: "ベーステーマ"
|
||||
light-theme: "Theme during non-dark mode"
|
||||
dark-theme: "Theme during dark mode"
|
||||
light-themes: "Light theme"
|
||||
dark-themes: "Dark theme"
|
||||
install-a-theme: "Install a theme"
|
||||
theme-code: "Theme code"
|
||||
install: "Install"
|
||||
installed: "\"{}\" has been installed"
|
||||
create-a-theme: "Create a theme"
|
||||
save-created-theme: "Save a theme"
|
||||
primary-color: "Primary color"
|
||||
secondary-color: "Secondary color"
|
||||
text-color: "Text color"
|
||||
base-theme: "Base theme"
|
||||
base-theme-light: "Light"
|
||||
base-theme-dark: "Dark"
|
||||
theme-name: "テーマ名"
|
||||
preview-created-theme: "プレビュー"
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
uninstalled: "「{}」をアンインストールしました"
|
||||
author: "作者"
|
||||
desc: "説明"
|
||||
export: "エクスポート"
|
||||
import: "インポート"
|
||||
import-by-code: "またはコードをペースト"
|
||||
theme-name-required: "テーマ名は必須です。"
|
||||
theme-name: "Theme name"
|
||||
preview-created-theme: "Preview"
|
||||
invalid-theme: "Not valid theme"
|
||||
already-installed: "This theme is already installed."
|
||||
saved: "Saved"
|
||||
manage-themes: "Themes manager"
|
||||
builtin-themes: "Standard themes"
|
||||
my-themes: "My themes"
|
||||
installed-themes: "Installed themes"
|
||||
select-theme: "Select your theme"
|
||||
uninstall: "Uninstall"
|
||||
uninstalled: "\"{}\" has been uninstalled"
|
||||
author: "Author"
|
||||
desc: "Description"
|
||||
export: "Export"
|
||||
import: "Import"
|
||||
import-by-code: "or paste code"
|
||||
theme-name-required: "Theme name is required"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "Hide"
|
||||
show: "See more"
|
||||
@ -335,8 +335,9 @@ common/views/components/note-menu.vue:
|
||||
detail: "Details"
|
||||
copy-link: "Copy link"
|
||||
favorite: "Favorite this note"
|
||||
unfavorite: "Unfavorite"
|
||||
pin: "Pin to your profile"
|
||||
unpin: "ピン留め解除"
|
||||
unpin: "Unpin"
|
||||
delete: "Delete"
|
||||
delete-confirm: "Delete this post?"
|
||||
remote: "Show original note"
|
||||
@ -514,13 +515,13 @@ desktop/views/components/charts.vue:
|
||||
notes: "The number of posts: increase/decrease (Combined)"
|
||||
local-notes: "The number of posts: increase/decrease (Local)"
|
||||
remote-notes: "The number of posts: increase/decrease (Remote)"
|
||||
notes-total: "投稿の積算"
|
||||
notes-total: "Total posts"
|
||||
users: "The number of users: increase/decrease"
|
||||
users-total: "ユーザーの積算"
|
||||
users-total: "Total users"
|
||||
drive: "Capacity used as the storage: increase/decrease"
|
||||
drive-total: "ドライブ使用量の積算"
|
||||
drive-total: "Total usage of Drive"
|
||||
drive-files: "The number of files on the storage: increase/decrease"
|
||||
drive-files-total: "ドライブのファイル数の積算"
|
||||
drive-files-total: "Total number of files on Drive"
|
||||
network-requests: "Requests"
|
||||
network-time: "Response time"
|
||||
network-usage: "Traffic"
|
||||
@ -730,8 +731,8 @@ desktop/views/components/settings.vue:
|
||||
choose-wallpaper: "Choose a background"
|
||||
delete-wallpaper: "Remove background"
|
||||
dark-mode: "Dark Mode"
|
||||
use-shadow: "UIに影を使用"
|
||||
rounded-corners: "UIの角を丸める"
|
||||
use-shadow: "Use shadows in the UI"
|
||||
rounded-corners: "Round corners of UI"
|
||||
circle-icons: "Use circle icons"
|
||||
contrasted-acct: "Add contrast to username"
|
||||
post-form-on-timeline: "Display post form at the top of the timeline"
|
||||
|
@ -335,6 +335,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "Detalles"
|
||||
copy-link: "Copiar enlace"
|
||||
favorite: "Me gusta esta nota"
|
||||
unfavorite: "お気に入り解除"
|
||||
pin: "Fijar en el perfil"
|
||||
unpin: "ピン留め解除"
|
||||
delete: "Borrar"
|
||||
|
@ -34,7 +34,7 @@ common:
|
||||
paragraph4: "Pour terminer la personnalisation, cliquez sur \"Terminer\" dans le coin supérieur droit."
|
||||
gotit: "Compris !"
|
||||
notification:
|
||||
file-uploaded: "Le fichier a été téléversé !"
|
||||
file-uploaded: "Le fichier a été transféré !"
|
||||
message-from: "Message de {} :"
|
||||
reversi-invited: "Invité à jouer"
|
||||
reversi-invited-by: "Invité par {} :"
|
||||
@ -136,7 +136,7 @@ common:
|
||||
memo: "Pense-bête"
|
||||
trends: "Tendances"
|
||||
photo-stream: "Flux de photos"
|
||||
posts-monitor: "Graphe des publications"
|
||||
posts-monitor: "Graph des publications"
|
||||
slideshow: "Diaporama"
|
||||
version: "Version"
|
||||
broadcast: "Diffusion"
|
||||
@ -335,6 +335,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "Détails"
|
||||
copy-link: "Copier le lien"
|
||||
favorite: "Mettre cette note en favoris"
|
||||
unfavorite: "お気に入り解除"
|
||||
pin: "Épingler sur votre profil"
|
||||
unpin: "Désépingler"
|
||||
delete: "Supprimer"
|
||||
@ -434,7 +435,7 @@ common/views/widgets/photo-stream.vue:
|
||||
title: "Flux de photos"
|
||||
no-photos: "Pas de photo"
|
||||
common/views/widgets/posts-monitor.vue:
|
||||
title: "Graphe des publications"
|
||||
title: "Graph des publications"
|
||||
toggle: "Basculer entre les vues"
|
||||
common/views/widgets/hashtags.vue:
|
||||
title: "Hashtags"
|
||||
@ -582,7 +583,7 @@ desktop/views/components/drive.vue:
|
||||
unable-to-process: "L'opération n'a pas pu être complétée"
|
||||
circular-reference-detected: "Le dossier de destination est un sous-dossier du dossier que vous souhaitez déplacer."
|
||||
unhandled-error: "Erreur inconnue"
|
||||
url-upload: "Uploader d'un URL"
|
||||
url-upload: "Téléverser via une URL"
|
||||
url-of-file: "URL de l'image que vous souhaitez uploader."
|
||||
url-upload-requested: "Upload requested"
|
||||
may-take-time: "L'upload de votre fichier peut prendre un certain temps."
|
||||
@ -590,8 +591,8 @@ desktop/views/components/drive.vue:
|
||||
folder-name: "Nom du dossier"
|
||||
contextmenu:
|
||||
create-folder: "Créer un dossier"
|
||||
upload: "Uploader un fichier"
|
||||
url-upload: "Uploader d'un URL"
|
||||
upload: "Transférer un fichier"
|
||||
url-upload: "Transférer à partir d’une URL"
|
||||
desktop/views/components/media-image.vue:
|
||||
sensitive: "Le contenu est NSFW"
|
||||
click-to-show: "Cliquer pour afficher"
|
||||
@ -658,21 +659,21 @@ desktop/views/components/post-form.vue:
|
||||
add-visible-user: "+Ajouter un utilisateur"
|
||||
attach-location-information: "Attacher des informations de localisation"
|
||||
hide-contents: "Masquer les contenus"
|
||||
reply-placeholder: "Répondre à cette note"
|
||||
quote-placeholder: "Citer cette note"
|
||||
submit: "Poster"
|
||||
reply-placeholder: "Répondre à cette note …"
|
||||
quote-placeholder: "Citer cette note …"
|
||||
submit: "Publier"
|
||||
reply: "Répondre"
|
||||
renote: "Republier"
|
||||
posted: "Posté!"
|
||||
replied: "Répondu!"
|
||||
reposted: "Reposté!"
|
||||
posted: "Publié !"
|
||||
replied: "Répondu !"
|
||||
reposted: "Reposté !"
|
||||
note-failed: "La note à échoué"
|
||||
reply-failed: "La réponse à échoué"
|
||||
renote-failed: "La renote à échoué"
|
||||
posting: "Publication..."
|
||||
attach-media-from-local: "Joindre un media depuis votre PC"
|
||||
attach-media-from-drive: "Joindre un media depuis votre Drive"
|
||||
attach-cancel: "Annuler la jointure de fichier"
|
||||
renote-failed: "Échec lors de la republication"
|
||||
posting: "Publication …"
|
||||
attach-media-from-local: "Joindre un média depuis votre appareil"
|
||||
attach-media-from-drive: "Joindre un média depuis votre Drive"
|
||||
attach-cancel: "Annuler le fichier attaché"
|
||||
insert-a-kao: "v('ω')v"
|
||||
create-poll: "Créer un sondage"
|
||||
text-remain: "{} charactères restants"
|
||||
@ -687,15 +688,15 @@ desktop/views/components/post-form-window.vue:
|
||||
note: "Nouvelle note"
|
||||
reply: "Répondre"
|
||||
attaches: "{} media joint(s)"
|
||||
uploading-media: "Upload du media {}"
|
||||
uploading-media: "Transfert du média {}"
|
||||
desktop/views/components/progress-dialog.vue:
|
||||
waiting: "En attente"
|
||||
desktop/views/components/renote-form.vue:
|
||||
quote: "Citer..."
|
||||
cancel: "Annuler"
|
||||
renote: "Republier"
|
||||
reposting: "Repost en cours..."
|
||||
success: "Reposté!"
|
||||
reposting: "Republication en cours …"
|
||||
success: "Republié !"
|
||||
failure: "La renote a échoué"
|
||||
desktop/views/components/renote-form-window.vue:
|
||||
title: "Êtes vous sûr de vouloir renote cette note?"
|
||||
@ -878,7 +879,7 @@ desktop/views/components/ui.header.nav.vue:
|
||||
desktop/views/components/ui.header.notifications.vue:
|
||||
title: "Notifications"
|
||||
desktop/views/components/ui.header.post.vue:
|
||||
post: "Composer un nouveau post"
|
||||
post: "Rédiger une nouvelle publication"
|
||||
desktop/views/components/ui.header.search.vue:
|
||||
placeholder: "Chercher"
|
||||
desktop/views/components/received-follow-requests-window.vue:
|
||||
@ -969,7 +970,7 @@ desktop/views/pages/selectdrive.vue:
|
||||
title: "Choisir fichier(s)"
|
||||
ok: "OK"
|
||||
cancel: "Annuler"
|
||||
upload: "Uploader un ou plusieurs fichier(s) depuis votre PC"
|
||||
upload: "Téléverser des fichiers à partir de votre ordinateur"
|
||||
desktop/views/pages/search.vue:
|
||||
not-available: "La fonction de recherche est désactivée dans les paramètres de l’instance."
|
||||
not-found: "Aucun message trouvé pour '{}'"
|
||||
@ -1029,8 +1030,8 @@ desktop/views/widgets/polls.vue:
|
||||
refresh: "Afficher d'autres"
|
||||
nothing: "Rien"
|
||||
desktop/views/widgets/post-form.vue:
|
||||
title: "Post"
|
||||
note: "Post"
|
||||
title: "Publication"
|
||||
note: "Publication"
|
||||
desktop/views/widgets/profile.vue:
|
||||
update-banner: "Cliquer pour éditer votre bannière"
|
||||
update-avatar: "Cliquer pour éditer votre avatar"
|
||||
@ -1092,7 +1093,7 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "Voir plus"
|
||||
close: "Fermer"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "Renoté par {}"
|
||||
reposted-by: "Republié par {}"
|
||||
private: "cette publication est privée"
|
||||
deleted: "cette publication a été supprimée"
|
||||
location: "Géolocalisation"
|
||||
@ -1119,7 +1120,7 @@ mobile/views/components/notifications.vue:
|
||||
empty: "Pas de notifications"
|
||||
mobile/views/components/post-form.vue:
|
||||
add-visible-user: "Ajouter un utilisateur"
|
||||
submit: "Poster"
|
||||
submit: "Publier"
|
||||
reply: "Répondre"
|
||||
renote: "Republier"
|
||||
quote-placeholder: "Citer ce billet ... (Facultatif)"
|
||||
|
@ -335,6 +335,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "詳細"
|
||||
copy-link: "リンクをコピー"
|
||||
favorite: "お気に入り"
|
||||
unfavorite: "お気に入り解除"
|
||||
pin: "ピン留め"
|
||||
unpin: "ピン留め解除"
|
||||
delete: "削除"
|
||||
|
@ -363,6 +363,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "詳細"
|
||||
copy-link: "リンクをコピー"
|
||||
favorite: "お気に入り"
|
||||
unfavorite: "お気に入り解除"
|
||||
pin: "ピン留め"
|
||||
unpin: "ピン留め解除"
|
||||
delete: "削除"
|
||||
@ -937,6 +938,7 @@ desktop/views/components/settings.profile.vue:
|
||||
save: "保存"
|
||||
locked-account: "アカウントの保護"
|
||||
is-locked: "フォローを承認制にする"
|
||||
careful-bot: "Botからのフォローだけ承認制にする"
|
||||
other: "その他"
|
||||
is-bot: "このアカウントはBotです"
|
||||
is-cat: "このアカウントはCatです"
|
||||
@ -1419,6 +1421,7 @@ mobile/views/pages/settings/settings.profile.vue:
|
||||
banner: "バナー"
|
||||
is-cat: "このアカウントはCatです"
|
||||
is-locked: "フォローを承認制にする"
|
||||
careful-bot: "Botからのフォローだけ承認制にする"
|
||||
advanced: "その他"
|
||||
privacy: "プライバシー"
|
||||
save: "保存"
|
||||
|
@ -265,40 +265,40 @@ common/views/components/media-banner.vue:
|
||||
sensitive: "見せたらあかん"
|
||||
click-to-show: "押してみ、見せたるわ"
|
||||
common/views/components/theme.vue:
|
||||
light-theme: "非ダークモード時に使用するテーマ"
|
||||
dark-theme: "ダークモード時に使用するテーマ"
|
||||
light-themes: "明るいテーマ"
|
||||
dark-themes: "暗いテーマ"
|
||||
install-a-theme: "テーマのインストール"
|
||||
light-theme: "ナイトゲームちゃう時のテーマどないする?"
|
||||
dark-theme: "ナイトゲームの時のテーマどないする?"
|
||||
light-themes: "デイゲーム"
|
||||
dark-themes: "ナイトゲーム"
|
||||
install-a-theme: "テーマ入れるで"
|
||||
theme-code: "テーマコード"
|
||||
install: "インストール"
|
||||
installed: "「{}」をインストールしました"
|
||||
create-a-theme: "テーマの作成"
|
||||
save-created-theme: "テーマを保存"
|
||||
primary-color: "プライマリ カラー"
|
||||
secondary-color: "セカンダリ カラー"
|
||||
text-color: "文字色"
|
||||
base-theme: "ベーステーマ"
|
||||
installed: "「{}」を入れたで!"
|
||||
create-a-theme: "テーマ作る"
|
||||
save-created-theme: "テーマ保存"
|
||||
primary-color: "この色一番重要や"
|
||||
secondary-color: "次はこの色出したって"
|
||||
text-color: "文字はこの色や!"
|
||||
base-theme: "この色が背景や!"
|
||||
base-theme-light: "Light"
|
||||
base-theme-dark: "Dark"
|
||||
theme-name: "テーマ名"
|
||||
preview-created-theme: "プレビュー"
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
preview-created-theme: "試してみる"
|
||||
invalid-theme: "このテーマあかんわ、なんか間違うとる"
|
||||
already-installed: "このテーマもうあるで"
|
||||
saved: "保存したで!"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
uninstalled: "「{}」をアンインストールしました"
|
||||
author: "作者"
|
||||
builtin-themes: "いつものテーマ"
|
||||
my-themes: "ワイのテーマ"
|
||||
installed-themes: "入れたテーマ"
|
||||
select-theme: "テーマ選んでや!"
|
||||
uninstall: "ほかす"
|
||||
uninstalled: "「{}」をほかしてもうたわ"
|
||||
author: "作った人"
|
||||
desc: "説明"
|
||||
export: "エクスポート"
|
||||
import: "インポート"
|
||||
import-by-code: "またはコードをペースト"
|
||||
theme-name-required: "テーマ名は必須です。"
|
||||
import-by-code: "それかコードを貼っつける"
|
||||
theme-name-required: "テーマ名は絶対要るで"
|
||||
common/views/components/cw-button.vue:
|
||||
hide: "もうええわ"
|
||||
show: "見たいやろ?"
|
||||
@ -335,6 +335,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "もっと"
|
||||
copy-link: "リンクをコピー"
|
||||
favorite: "お気に入り"
|
||||
unfavorite: "お気に入りやめる"
|
||||
pin: "ピン留め"
|
||||
unpin: "ピン留めやめる"
|
||||
delete: "ほかす"
|
||||
@ -475,7 +476,7 @@ common/views/pages/follow.vue:
|
||||
following: "フォローしとる"
|
||||
follow: "フォロー"
|
||||
request-pending: "フォローの許し待っとる"
|
||||
follow-processing: "フォロー処理中"
|
||||
follow-processing: "今フォロー処理やっとる‥"
|
||||
follow-request: "フォロー許してくれや!言うてみる"
|
||||
desktop:
|
||||
banner-crop-title: "どこバナーとして出す?"
|
||||
@ -602,7 +603,7 @@ desktop/views/components/follow-button.vue:
|
||||
following: "フォローしとる"
|
||||
follow: "フォロー"
|
||||
request-pending: "フォローの許し待っとる"
|
||||
follow-processing: "フォロー処理中"
|
||||
follow-processing: "今フォロー処理やっとる‥"
|
||||
follow-request: "フォロー許してくれや!言うてみる"
|
||||
desktop/views/components/followers-window.vue:
|
||||
followers: "{} のフォロワー"
|
||||
@ -1083,7 +1084,7 @@ mobile/views/components/follow-button.vue:
|
||||
following: "フォローしとる"
|
||||
follow: "フォロー"
|
||||
request-pending: "フォローの許し待っとる"
|
||||
follow-processing: "フォロー処理中"
|
||||
follow-processing: "今フォロー処理やっとる‥"
|
||||
follow-request: "フォロー許してくれや!言うてみる"
|
||||
mobile/views/components/friends-maker.vue:
|
||||
title: "おもろそうやな"
|
||||
@ -1223,9 +1224,9 @@ mobile/views/pages/settings/settings.profile.vue:
|
||||
avatar: "アイコン"
|
||||
banner: "バナー"
|
||||
is-cat: "このアカウントはCatや"
|
||||
is-locked: "他人のフォローは許してからや!"
|
||||
is-locked: "他人のフォローは許可してからや!"
|
||||
advanced: "その他"
|
||||
privacy: "プライバシー⇔オカンの年齢"
|
||||
privacy: "プライバシーってなんや?オカンの年齢か?"
|
||||
save: "保存"
|
||||
saved: "プロフィールを保存したで"
|
||||
uploading: "アップロードしとるで…"
|
||||
@ -1245,7 +1246,7 @@ mobile/views/pages/settings.vue:
|
||||
specify-language: "言語選びや"
|
||||
design: "見た感じ"
|
||||
dark-mode: "ナイトゲームや!"
|
||||
i-am-under-limited-internet: "電波がバァーっといけへんねん"
|
||||
i-am-under-limited-internet: "電波と阪神がザコいんや"
|
||||
circle-icons: "アイコンもタコ焼きも丸いやんな?"
|
||||
contrasted-acct: "ユーザー名ようわからんし見やすしといて"
|
||||
timeline: "タイムライン"
|
||||
@ -1257,8 +1258,8 @@ mobile/views/pages/settings.vue:
|
||||
post-style-standard: "標準"
|
||||
post-style-smart: "べっぴんさん"
|
||||
notification-position: "通知どこ見せる?"
|
||||
notification-position-bottom: "ミナミ"
|
||||
notification-position-top: "キタ"
|
||||
notification-position-bottom: "ミナミの方"
|
||||
notification-position-top: "キタの方"
|
||||
theme: "テーマ"
|
||||
behavior: "動き"
|
||||
fetch-on-scroll: "スクロールしたらもっと見せてや"
|
||||
|
@ -335,6 +335,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "詳細"
|
||||
copy-link: "링크 복사"
|
||||
favorite: "お気に入り"
|
||||
unfavorite: "お気に入り解除"
|
||||
pin: "ピン留め"
|
||||
unpin: "ピン留め解除"
|
||||
delete: "削除"
|
||||
|
@ -335,6 +335,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "詳細"
|
||||
copy-link: "リンクをコピー"
|
||||
favorite: "Deze notitie toevoegen aan favorieten"
|
||||
unfavorite: "お気に入り解除"
|
||||
pin: "Vastmaken aan profielpagina"
|
||||
unpin: "ピン留め解除"
|
||||
delete: "削除"
|
||||
|
@ -335,6 +335,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "Detaljer"
|
||||
copy-link: "リンクをコピー"
|
||||
favorite: "Merket som favoritt"
|
||||
unfavorite: "お気に入り解除"
|
||||
pin: "Fest til profilen din"
|
||||
unpin: "ピン留め解除"
|
||||
delete: "Slett"
|
||||
|
@ -335,6 +335,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "詳細"
|
||||
copy-link: "リンクをコピー"
|
||||
favorite: "Dodaj do ulubionych"
|
||||
unfavorite: "お気に入り解除"
|
||||
pin: "Przypnij do profilu"
|
||||
unpin: "ピン留め解除"
|
||||
delete: "Usuń"
|
||||
|
@ -335,6 +335,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "詳細"
|
||||
copy-link: "リンクをコピー"
|
||||
favorite: "お気に入り"
|
||||
unfavorite: "お気に入り解除"
|
||||
pin: "ピン留め"
|
||||
unpin: "ピン留め解除"
|
||||
delete: "削除"
|
||||
|
@ -335,6 +335,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "詳細"
|
||||
copy-link: "リンクをコピー"
|
||||
favorite: "お気に入り"
|
||||
unfavorite: "お気に入り解除"
|
||||
pin: "ピン留め"
|
||||
unpin: "ピン留め解除"
|
||||
delete: "削除"
|
||||
|
@ -335,6 +335,7 @@ common/views/components/note-menu.vue:
|
||||
detail: "詳細"
|
||||
copy-link: "リンクをコピー"
|
||||
favorite: "お気に入り"
|
||||
unfavorite: "お気に入り解除"
|
||||
pin: "ピン留め"
|
||||
unpin: "ピン留め解除"
|
||||
delete: "削除"
|
||||
|
14
package.json
14
package.json
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "10.6.0",
|
||||
"clientVersion": "1.0.10417",
|
||||
"version": "10.14.0",
|
||||
"clientVersion": "1.0.10524",
|
||||
"codename": "nighthike",
|
||||
"main": "./built/index.js",
|
||||
"private": true,
|
||||
@ -32,7 +32,7 @@
|
||||
"@types/debug": "0.0.31",
|
||||
"@types/deep-equal": "1.0.1",
|
||||
"@types/double-ended-queue": "2.1.0",
|
||||
"@types/elasticsearch": "5.0.26",
|
||||
"@types/elasticsearch": "5.0.27",
|
||||
"@types/file-type": "5.2.1",
|
||||
"@types/gulp": "3.8.36",
|
||||
"@types/gulp-htmlmin": "1.3.32",
|
||||
@ -60,7 +60,7 @@
|
||||
"@types/mocha": "5.2.3",
|
||||
"@types/mongodb": "3.1.12",
|
||||
"@types/ms": "0.7.30",
|
||||
"@types/node": "10.11.6",
|
||||
"@types/node": "10.11.7",
|
||||
"@types/portscanner": "2.1.0",
|
||||
"@types/pug": "2.0.4",
|
||||
"@types/qrcode": "1.3.0",
|
||||
@ -169,13 +169,14 @@
|
||||
"parse5": "5.1.0",
|
||||
"portscanner": "2.2.0",
|
||||
"progress-bar-webpack-plugin": "1.11.0",
|
||||
"promise-limit": "2.7.0",
|
||||
"promise-sequential": "1.1.1",
|
||||
"pug": "2.0.3",
|
||||
"punycode": "2.1.1",
|
||||
"qrcode": "1.3.0",
|
||||
"ratelimiter": "3.2.0",
|
||||
"recaptcha-promise": "0.1.3",
|
||||
"reconnecting-websocket": "4.1.5",
|
||||
"reconnecting-websocket": "4.1.8",
|
||||
"redis": "2.8.0",
|
||||
"request": "2.88.0",
|
||||
"request-promise-native": "1.0.5",
|
||||
@ -211,7 +212,8 @@
|
||||
"v-animate-css": "0.0.2",
|
||||
"vue": "2.5.17",
|
||||
"vue-chartjs": "3.4.0",
|
||||
"vue-color": "2.6.0",
|
||||
"vue-color": "2.7.0",
|
||||
"vue-content-loading": "1.5.3",
|
||||
"vue-cropperjs": "2.2.2",
|
||||
"vue-js-modal": "1.3.26",
|
||||
"vue-json-tree-view": "2.1.4",
|
||||
|
@ -142,7 +142,7 @@
|
||||
localStorage.setItem('shouldFlush', 'false');
|
||||
|
||||
// Random
|
||||
localStorage.setItem('salt', Math.random().toString());
|
||||
localStorage.setItem('salt', Math.random().toString().substr(2, 8));
|
||||
|
||||
// Clear cache (service worker)
|
||||
try {
|
||||
|
178
src/client/app/common/scripts/note-mixin.ts
Normal file
178
src/client/app/common/scripts/note-mixin.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import parse from '../../../../mfm/parse';
|
||||
import { sum } from '../../../../prelude/array';
|
||||
import MkNoteMenu from '../views/components/note-menu.vue';
|
||||
import MkReactionPicker from '../views/components/reaction-picker.vue';
|
||||
import Ok from '../views/components/ok.vue';
|
||||
|
||||
function focus(el, fn) {
|
||||
const target = fn(el);
|
||||
if (target) {
|
||||
if (target.hasAttribute('tabindex')) {
|
||||
target.focus();
|
||||
} else {
|
||||
focus(target, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Opts = {
|
||||
mobile?: boolean;
|
||||
};
|
||||
|
||||
export default (opts: Opts = {}) => ({
|
||||
data() {
|
||||
return {
|
||||
showContent: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'r|left': () => this.reply(true),
|
||||
'e|a|plus': () => this.react(true),
|
||||
'q|right': () => this.renote(true),
|
||||
'f|b': this.favorite,
|
||||
'delete|ctrl+d': this.del,
|
||||
'ctrl+q|ctrl+right': this.renoteDirectly,
|
||||
'up|k|shift+tab': this.focusBefore,
|
||||
'down|j|tab': this.focusAfter,
|
||||
'esc': this.blur,
|
||||
'm|o': () => this.menu(true),
|
||||
's': this.toggleShowContent,
|
||||
'1': () => this.reactDirectly('like'),
|
||||
'2': () => this.reactDirectly('love'),
|
||||
'3': () => this.reactDirectly('laugh'),
|
||||
'4': () => this.reactDirectly('hmm'),
|
||||
'5': () => this.reactDirectly('surprise'),
|
||||
'6': () => this.reactDirectly('congrats'),
|
||||
'7': () => this.reactDirectly('angry'),
|
||||
'8': () => this.reactDirectly('confused'),
|
||||
'9': () => this.reactDirectly('rip'),
|
||||
'0': () => this.reactDirectly('pudding'),
|
||||
};
|
||||
},
|
||||
|
||||
isRenote(): boolean {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
this.note.fileIds.length == 0 &&
|
||||
this.note.poll == null);
|
||||
},
|
||||
|
||||
appearNote(): any {
|
||||
return this.isRenote ? this.note.renote : this.note;
|
||||
},
|
||||
|
||||
reactionsCount(): number {
|
||||
return this.appearNote.reactionCounts
|
||||
? sum(Object.values(this.appearNote.reactionCounts))
|
||||
: 0;
|
||||
},
|
||||
|
||||
title(): string {
|
||||
return new Date(this.appearNote.createdAt).toLocaleString();
|
||||
},
|
||||
|
||||
urls(): string[] {
|
||||
if (this.appearNote.text) {
|
||||
const ast = parse(this.appearNote.text);
|
||||
return ast
|
||||
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
|
||||
.map(t => t.url);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
reply(viaKeyboard = false) {
|
||||
(this as any).apis.post({
|
||||
reply: this.appearNote,
|
||||
animation: !viaKeyboard,
|
||||
cb: () => {
|
||||
this.focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
renote(viaKeyboard = false) {
|
||||
(this as any).apis.post({
|
||||
renote: this.appearNote,
|
||||
animation: !viaKeyboard,
|
||||
cb: () => {
|
||||
this.focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
renoteDirectly() {
|
||||
(this as any).api('notes/create', {
|
||||
renoteId: this.appearNote.id
|
||||
});
|
||||
},
|
||||
|
||||
react(viaKeyboard = false) {
|
||||
this.blur();
|
||||
(this as any).os.new(MkReactionPicker, {
|
||||
source: this.$refs.reactButton,
|
||||
note: this.appearNote,
|
||||
showFocus: viaKeyboard,
|
||||
animation: !viaKeyboard,
|
||||
compact: opts.mobile,
|
||||
big: opts.mobile
|
||||
}).$once('closed', this.focus);
|
||||
},
|
||||
|
||||
reactDirectly(reaction) {
|
||||
(this as any).api('notes/reactions/create', {
|
||||
noteId: this.appearNote.id,
|
||||
reaction: reaction
|
||||
});
|
||||
},
|
||||
|
||||
favorite() {
|
||||
(this as any).api('notes/favorites/create', {
|
||||
noteId: this.appearNote.id
|
||||
}).then(() => {
|
||||
(this as any).os.new(Ok);
|
||||
});
|
||||
},
|
||||
|
||||
del() {
|
||||
(this as any).api('notes/delete', {
|
||||
noteId: this.appearNote.id
|
||||
});
|
||||
},
|
||||
|
||||
menu(viaKeyboard = false) {
|
||||
(this as any).os.new(MkNoteMenu, {
|
||||
source: this.$refs.menuButton,
|
||||
note: this.appearNote,
|
||||
animation: !viaKeyboard,
|
||||
compact: opts.mobile,
|
||||
}).$once('closed', this.focus);
|
||||
},
|
||||
|
||||
toggleShowContent() {
|
||||
this.showContent = !this.showContent;
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.$el.focus();
|
||||
},
|
||||
|
||||
blur() {
|
||||
this.$el.blur();
|
||||
},
|
||||
|
||||
focusBefore() {
|
||||
focus(this.$el, e => e.previousElementSibling);
|
||||
},
|
||||
|
||||
focusAfter() {
|
||||
focus(this.$el, e => e.nextElementSibling);
|
||||
}
|
||||
}
|
||||
});
|
@ -9,8 +9,7 @@ import MiOS from '../../mios';
|
||||
*/
|
||||
export default class Stream extends EventEmitter {
|
||||
private stream: ReconnectingWebsocket;
|
||||
private state: string;
|
||||
private buffer: any[];
|
||||
public state: string;
|
||||
private sharedConnectionPools: Pool[] = [];
|
||||
private sharedConnections: SharedConnection[] = [];
|
||||
private nonSharedConnections: NonSharedConnection[] = [];
|
||||
@ -19,7 +18,6 @@ export default class Stream extends EventEmitter {
|
||||
super();
|
||||
|
||||
this.state = 'initializing';
|
||||
this.buffer = [];
|
||||
|
||||
const user = os.store.state.i;
|
||||
|
||||
@ -29,7 +27,8 @@ export default class Stream extends EventEmitter {
|
||||
this.stream.addEventListener('message', this.onMessage);
|
||||
}
|
||||
|
||||
public useSharedConnection = (channel: string): SharedConnection => {
|
||||
@autobind
|
||||
public useSharedConnection(channel: string): SharedConnection {
|
||||
let pool = this.sharedConnectionPools.find(p => p.channel === channel);
|
||||
|
||||
if (pool == null) {
|
||||
@ -47,7 +46,13 @@ export default class Stream extends EventEmitter {
|
||||
this.sharedConnections = this.sharedConnections.filter(c => c !== connection);
|
||||
}
|
||||
|
||||
public connectToChannel = (channel: string, params?: any): NonSharedConnection => {
|
||||
@autobind
|
||||
public removeSharedConnectionPool(pool: Pool) {
|
||||
this.sharedConnectionPools = this.sharedConnectionPools.filter(p => p !== pool);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public connectToChannel(channel: string, params?: any): NonSharedConnection {
|
||||
const connection = new NonSharedConnection(this, channel, params);
|
||||
this.nonSharedConnections.push(connection);
|
||||
return connection;
|
||||
@ -68,13 +73,6 @@ export default class Stream extends EventEmitter {
|
||||
this.state = 'connected';
|
||||
this.emit('_connected_');
|
||||
|
||||
// バッファーを処理
|
||||
const _buffer = [].concat(this.buffer); // Shallow copy
|
||||
this.buffer = []; // Clear buffer
|
||||
_buffer.forEach(data => {
|
||||
this.send(data); // Resend each buffered messages
|
||||
});
|
||||
|
||||
// チャンネル再接続
|
||||
if (isReconnect) {
|
||||
this.sharedConnectionPools.forEach(p => {
|
||||
@ -91,8 +89,10 @@ export default class Stream extends EventEmitter {
|
||||
*/
|
||||
@autobind
|
||||
private onClose() {
|
||||
this.state = 'reconnecting';
|
||||
this.emit('_disconnected_');
|
||||
if (this.state == 'connected') {
|
||||
this.state = 'reconnecting';
|
||||
this.emit('_disconnected_');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -131,12 +131,6 @@ export default class Stream extends EventEmitter {
|
||||
body: payload
|
||||
};
|
||||
|
||||
// まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
|
||||
if (this.state != 'connected') {
|
||||
this.buffer.push(data);
|
||||
return;
|
||||
}
|
||||
|
||||
this.stream.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
@ -154,7 +148,7 @@ class Pool {
|
||||
public channel: string;
|
||||
public id: string;
|
||||
protected stream: Stream;
|
||||
private users = 0;
|
||||
public users = 0;
|
||||
private disposeTimerId: any;
|
||||
private isConnected = false;
|
||||
|
||||
@ -162,7 +156,14 @@ class Pool {
|
||||
this.channel = channel;
|
||||
this.stream = stream;
|
||||
|
||||
this.id = Math.random().toString();
|
||||
this.id = Math.random().toString().substr(2, 8);
|
||||
|
||||
this.stream.on('_disconnected_', this.onStreamDisconnected);
|
||||
}
|
||||
|
||||
@autobind
|
||||
private onStreamDisconnected() {
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
@autobind
|
||||
@ -196,6 +197,7 @@ class Pool {
|
||||
|
||||
@autobind
|
||||
public connect() {
|
||||
if (this.isConnected) return;
|
||||
this.isConnected = true;
|
||||
this.stream.send('connect', {
|
||||
channel: this.channel,
|
||||
@ -205,9 +207,9 @@ class Pool {
|
||||
|
||||
@autobind
|
||||
private disconnect() {
|
||||
this.isConnected = false;
|
||||
this.disposeTimerId = null;
|
||||
this.stream.off('_disconnected_', this.onStreamDisconnected);
|
||||
this.stream.send('disconnect', { id: this.id });
|
||||
this.stream.removeSharedConnectionPool(this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,7 +275,7 @@ class NonSharedConnection extends Connection {
|
||||
super(stream, channel);
|
||||
|
||||
this.params = params;
|
||||
this.id = Math.random().toString();
|
||||
this.id = Math.random().toString().substr(2, 8);
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import noteSkeleton from './note-skeleton.vue';
|
||||
import theme from './theme.vue';
|
||||
import instance from './instance.vue';
|
||||
import cwButton from './cw-button.vue';
|
||||
@ -44,6 +45,7 @@ import uiSelect from './ui/select.vue';
|
||||
import formButton from './ui/form/button.vue';
|
||||
import formRadio from './ui/form/radio.vue';
|
||||
|
||||
Vue.component('mk-note-skeleton', noteSkeleton);
|
||||
Vue.component('mk-theme', theme);
|
||||
Vue.component('mk-instance', instance);
|
||||
Vue.component('mk-cw-button', cwButton);
|
||||
|
@ -354,7 +354,7 @@ export default Vue.extend({
|
||||
max-width 600px
|
||||
margin 0 auto
|
||||
padding 0
|
||||
//background rgba(var(--face), 0.95)
|
||||
background var(--messagingRoomBg)
|
||||
background-clip content-box
|
||||
|
||||
> .new-message
|
||||
|
@ -22,12 +22,34 @@ export default Vue.extend({
|
||||
icon: '%fa:link%',
|
||||
text: '%i18n:@copy-link%',
|
||||
action: this.copyLink
|
||||
}, null, {
|
||||
icon: '%fa:star%',
|
||||
text: '%i18n:@favorite%',
|
||||
action: this.favorite
|
||||
}];
|
||||
|
||||
if (this.note.uri) {
|
||||
items.push({
|
||||
icon: '%fa:external-link-square-alt%',
|
||||
text: '%i18n:@remote%',
|
||||
action: () => {
|
||||
window.open(this.note.uri, '_blank');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
items.push(null);
|
||||
|
||||
if (this.note.isFavorited) {
|
||||
items.push({
|
||||
icon: '%fa:star%',
|
||||
text: '%i18n:@unfavorite%',
|
||||
action: this.unfavorite
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
icon: '%fa:star%',
|
||||
text: '%i18n:@favorite%',
|
||||
action: this.favorite
|
||||
});
|
||||
}
|
||||
|
||||
if (this.note.userId == this.$store.state.i.id) {
|
||||
if ((this.$store.state.i.pinnedNoteIds || []).includes(this.note.id)) {
|
||||
items.push({
|
||||
@ -45,6 +67,7 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
if (this.note.userId == this.$store.state.i.id || this.$store.state.i.isAdmin) {
|
||||
items.push(null);
|
||||
items.push({
|
||||
icon: '%fa:trash-alt R%',
|
||||
text: '%i18n:@delete%',
|
||||
@ -52,16 +75,6 @@ export default Vue.extend({
|
||||
});
|
||||
}
|
||||
|
||||
if (this.note.uri) {
|
||||
items.push({
|
||||
icon: '%fa:external-link-square-alt%',
|
||||
text: '%i18n:@remote%',
|
||||
action: () => {
|
||||
window.open(this.note.uri, '_blank');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
},
|
||||
@ -110,6 +123,15 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
unfavorite() {
|
||||
(this as any).api('notes/favorites/delete', {
|
||||
noteId: this.note.id
|
||||
}).then(() => {
|
||||
(this as any).os.new(Ok);
|
||||
this.destroyDom();
|
||||
});
|
||||
},
|
||||
|
||||
closed() {
|
||||
this.$nextTick(() => {
|
||||
this.destroyDom();
|
||||
|
52
src/client/app/common/views/components/note-skeleton.vue
Normal file
52
src/client/app/common/views/components/note-skeleton.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<vue-content-loading v-if="width" :width="width" :height="100" :primary="primary" :secondary="secondary">
|
||||
<circle cx="30" cy="30" r="30" />
|
||||
<rect x="75" y="13" rx="4" ry="4" :width="150 + r1" height="15" />
|
||||
<rect x="75" y="39" rx="4" ry="4" :width="260 + r2" height="10" />
|
||||
<rect x="75" y="59" rx="4" ry="4" :width="230 + r3" height="10" />
|
||||
</vue-content-loading>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import VueContentLoading from 'vue-content-loading';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
VueContentLoading,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
width: 0,
|
||||
r1: (Math.random() * 100) - 50,
|
||||
r2: (Math.random() * 100) - 50,
|
||||
r3: (Math.random() * 100) - 50
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
text(): tinycolor.Instance {
|
||||
const text = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text'));
|
||||
return text;
|
||||
},
|
||||
|
||||
primary(): string {
|
||||
return '#' + this.text.clone().toHex();
|
||||
},
|
||||
|
||||
secondary(): string {
|
||||
return '#' + this.text.clone().darken(20).toHex();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
let width = this.$el.clientWidth;
|
||||
if (width < 400) width = 400;
|
||||
this.width = width;
|
||||
}
|
||||
});
|
||||
</script>
|
@ -114,7 +114,7 @@ export default define({
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
this.connection.send('requestLog',{
|
||||
id: Math.random().toString()
|
||||
id: Math.random().toString().substr(2, 8)
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
|
@ -92,7 +92,7 @@ export default Vue.extend({
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
this.connection.send('requestLog', {
|
||||
id: Math.random().toString()
|
||||
id: Math.random().toString().substr(2, 8)
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
|
@ -6,13 +6,17 @@ export default (os: OS) => opts => {
|
||||
const o = opts || {};
|
||||
if (o.renote) {
|
||||
const vm = os.new(RenoteFormWindow, {
|
||||
note: o.renote
|
||||
note: o.renote,
|
||||
animation: o.animation == null ? true : o.animation
|
||||
});
|
||||
if (o.cb) vm.$once('closed', o.cb);
|
||||
document.body.appendChild(vm.$el);
|
||||
} else {
|
||||
const vm = os.new(PostFormWindow, {
|
||||
reply: o.reply
|
||||
reply: o.reply,
|
||||
animation: o.animation == null ? true : o.animation
|
||||
});
|
||||
if (o.cb) vm.$once('closed', o.cb);
|
||||
document.body.appendChild(vm.$el);
|
||||
}
|
||||
};
|
||||
|
@ -1,37 +0,0 @@
|
||||
<template>
|
||||
<div class="mk-ellipsis-icon">
|
||||
<div></div><div></div><div></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-ellipsis-icon
|
||||
width 70px
|
||||
margin 0 auto
|
||||
text-align center
|
||||
|
||||
> div
|
||||
display inline-block
|
||||
width 18px
|
||||
height 18px
|
||||
background-color rgba(#000, 0.3)
|
||||
border-radius 100%
|
||||
animation bounce 1.4s infinite ease-in-out both
|
||||
|
||||
&:nth-child(1)
|
||||
animation-delay 0s
|
||||
|
||||
&:nth-child(2)
|
||||
margin 0 6px
|
||||
animation-delay 0.16s
|
||||
|
||||
&:nth-child(3)
|
||||
animation-delay 0.32s
|
||||
|
||||
@keyframes bounce
|
||||
0%, 80%, 100%
|
||||
transform scale(0)
|
||||
40%
|
||||
transform scale(1)
|
||||
|
||||
</style>
|
@ -9,7 +9,6 @@ import subNoteContent from './sub-note-content.vue';
|
||||
import window from './window.vue';
|
||||
import noteFormWindow from './post-form-window.vue';
|
||||
import renoteFormWindow from './renote-form-window.vue';
|
||||
import ellipsisIcon from './ellipsis-icon.vue';
|
||||
import mediaImage from './media-image.vue';
|
||||
import mediaImageDialog from './media-image-dialog.vue';
|
||||
import mediaVideo from './media-video.vue';
|
||||
@ -39,7 +38,6 @@ Vue.component('mk-sub-note-content', subNoteContent);
|
||||
Vue.component('mk-window', window);
|
||||
Vue.component('mk-post-form-window', noteFormWindow);
|
||||
Vue.component('mk-renote-form-window', renoteFormWindow);
|
||||
Vue.component('mk-ellipsis-icon', ellipsisIcon);
|
||||
Vue.component('mk-media-image', mediaImage);
|
||||
Vue.component('mk-media-image-dialog', mediaImageDialog);
|
||||
Vue.component('mk-media-video', mediaVideo);
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="note" tabindex="-1" v-hotkey="keymap" :title="title">
|
||||
<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
|
||||
<x-sub :note="p.reply"/>
|
||||
<div class="note" v-show="appearNote.deletedAt == null" :tabindex="appearNote.deletedAt == null ? '-1' : null" v-hotkey="keymap" :title="title">
|
||||
<div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
|
||||
<x-sub :note="appearNote.reply"/>
|
||||
</div>
|
||||
<div class="renote" v-if="isRenote">
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
@ -12,228 +12,76 @@
|
||||
<mk-time :time="note.createdAt"/>
|
||||
</div>
|
||||
<article>
|
||||
<mk-avatar class="avatar" :user="p.user"/>
|
||||
<mk-avatar class="avatar" :user="appearNote.user"/>
|
||||
<div class="main">
|
||||
<mk-note-header class="header" :note="p"/>
|
||||
<mk-note-header class="header" :note="appearNote"/>
|
||||
<div class="body">
|
||||
<p v-if="p.cw != null" class="cw">
|
||||
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
|
||||
<p v-if="appearNote.cw != null" class="cw">
|
||||
<span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span>
|
||||
<mk-cw-button v-model="showContent"/>
|
||||
</p>
|
||||
<div class="content" v-show="p.cw == null || showContent">
|
||||
<div class="content" v-show="appearNote.cw == null || showContent">
|
||||
<div class="text">
|
||||
<span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
|
||||
<span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
|
||||
<a class="reply" v-if="p.reply">%fa:reply%</a>
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/>
|
||||
<a class="rp" v-if="p.renote">RP:</a>
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">%i18n:@private%</span>
|
||||
<a class="reply" v-if="appearNote.reply">%fa:reply%</a>
|
||||
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text"/>
|
||||
<a class="rp" v-if="appearNote.renote">RP:</a>
|
||||
</div>
|
||||
<div class="files" v-if="p.files.length > 0">
|
||||
<mk-media-list :media-list="p.files"/>
|
||||
<div class="files" v-if="appearNote.files.length > 0">
|
||||
<mk-media-list :media-list="appearNote.files"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||
<a class="location" v-if="p.geo" :href="`https://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="renote" v-if="p.renote"><mk-note-preview :note="p.renote"/></div>
|
||||
<mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
|
||||
<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
|
||||
<div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
||||
</div>
|
||||
</div>
|
||||
<footer v-if="p.deletedAt == null">
|
||||
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
|
||||
<footer>
|
||||
<mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
|
||||
<button class="replyButton" @click="reply()" title="%i18n:@reply%">
|
||||
<template v-if="p.reply">%fa:reply-all%</template>
|
||||
<template v-if="appearNote.reply">%fa:reply-all%</template>
|
||||
<template v-else>%fa:reply%</template>
|
||||
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
|
||||
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<button class="renoteButton" @click="renote()" title="%i18n:@renote%">
|
||||
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
|
||||
%fa:retweet%<p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
|
||||
</button>
|
||||
<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react()" ref="reactButton" title="%i18n:@add-reaction%">
|
||||
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
|
||||
<button class="reactionButton" :class="{ reacted: appearNote.myReaction != null }" @click="react()" ref="reactButton" title="%i18n:@add-reaction%">
|
||||
%fa:plus%<p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p>
|
||||
</button>
|
||||
<button @click="menu()" ref="menuButton">
|
||||
%fa:ellipsis-h%
|
||||
</button>
|
||||
<!-- <button title="%i18n:@detail">
|
||||
<template v-if="!isDetailOpened">%fa:caret-down%</template>
|
||||
<template v-if="isDetailOpened">%fa:caret-up%</template>
|
||||
</button> -->
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
<div class="detail" v-if="isDetailOpened">
|
||||
<mk-note-status-graph width="462" height="130" :note="p"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import parse from '../../../../../mfm/parse';
|
||||
|
||||
import MkPostFormWindow from './post-form-window.vue';
|
||||
import MkRenoteFormWindow from './renote-form-window.vue';
|
||||
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
|
||||
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
|
||||
import XSub from './notes.note.sub.vue';
|
||||
import { sum } from '../../../../../prelude/array';
|
||||
import noteMixin from '../../../common/scripts/note-mixin';
|
||||
import noteSubscriber from '../../../common/scripts/note-subscriber';
|
||||
|
||||
function focus(el, fn) {
|
||||
const target = fn(el);
|
||||
if (target) {
|
||||
if (target.hasAttribute('tabindex')) {
|
||||
target.focus();
|
||||
} else {
|
||||
focus(target, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XSub
|
||||
},
|
||||
|
||||
mixins: [noteSubscriber('note')],
|
||||
mixins: [
|
||||
noteMixin(),
|
||||
noteSubscriber('note')
|
||||
],
|
||||
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showContent: false,
|
||||
isDetailOpened: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'r|left': () => this.reply(true),
|
||||
'e|a|plus': () => this.react(true),
|
||||
'q|right': () => this.renote(true),
|
||||
'ctrl+q|ctrl+right': this.renoteDirectly,
|
||||
'up|k|shift+tab': this.focusBefore,
|
||||
'down|j|tab': this.focusAfter,
|
||||
'esc': this.blur,
|
||||
'm|o': () => this.menu(true),
|
||||
's': this.toggleShowContent,
|
||||
'1': () => this.reactDirectly('like'),
|
||||
'2': () => this.reactDirectly('love'),
|
||||
'3': () => this.reactDirectly('laugh'),
|
||||
'4': () => this.reactDirectly('hmm'),
|
||||
'5': () => this.reactDirectly('surprise'),
|
||||
'6': () => this.reactDirectly('congrats'),
|
||||
'7': () => this.reactDirectly('angry'),
|
||||
'8': () => this.reactDirectly('confused'),
|
||||
'9': () => this.reactDirectly('rip'),
|
||||
'0': () => this.reactDirectly('pudding'),
|
||||
};
|
||||
},
|
||||
|
||||
isRenote(): boolean {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
this.note.fileIds.length == 0 &&
|
||||
this.note.poll == null);
|
||||
},
|
||||
|
||||
p(): any {
|
||||
return this.isRenote ? this.note.renote : this.note;
|
||||
},
|
||||
|
||||
reactionsCount(): number {
|
||||
return this.p.reactionCounts
|
||||
? sum(Object.values(this.p.reactionCounts))
|
||||
: 0;
|
||||
},
|
||||
|
||||
title(): string {
|
||||
return new Date(this.p.createdAt).toLocaleString();
|
||||
},
|
||||
|
||||
urls(): string[] {
|
||||
if (this.p.text) {
|
||||
const ast = parse(this.p.text);
|
||||
return ast
|
||||
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
|
||||
.map(t => t.url);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
reply(viaKeyboard = false) {
|
||||
(this as any).os.new(MkPostFormWindow, {
|
||||
reply: this.p,
|
||||
animation: !viaKeyboard
|
||||
}).$once('closed', this.focus);
|
||||
},
|
||||
|
||||
renote(viaKeyboard = false) {
|
||||
(this as any).os.new(MkRenoteFormWindow, {
|
||||
note: this.p,
|
||||
animation: !viaKeyboard
|
||||
}).$once('closed', this.focus);
|
||||
},
|
||||
|
||||
renoteDirectly() {
|
||||
(this as any).api('notes/create', {
|
||||
renoteId: this.p.id
|
||||
});
|
||||
},
|
||||
|
||||
react(viaKeyboard = false) {
|
||||
this.blur();
|
||||
(this as any).os.new(MkReactionPicker, {
|
||||
source: this.$refs.reactButton,
|
||||
note: this.p,
|
||||
showFocus: viaKeyboard,
|
||||
animation: !viaKeyboard
|
||||
}).$once('closed', this.focus);
|
||||
},
|
||||
|
||||
reactDirectly(reaction) {
|
||||
(this as any).api('notes/reactions/create', {
|
||||
noteId: this.p.id,
|
||||
reaction: reaction
|
||||
});
|
||||
},
|
||||
|
||||
menu(viaKeyboard = false) {
|
||||
(this as any).os.new(MkNoteMenu, {
|
||||
source: this.$refs.menuButton,
|
||||
note: this.p,
|
||||
animation: !viaKeyboard
|
||||
}).$once('closed', this.focus);
|
||||
},
|
||||
|
||||
toggleShowContent() {
|
||||
this.showContent = !this.showContent;
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.$el.focus();
|
||||
},
|
||||
|
||||
blur() {
|
||||
this.$el.blur();
|
||||
},
|
||||
|
||||
focusBefore() {
|
||||
focus(this.$el, e => e.previousElementSibling);
|
||||
},
|
||||
|
||||
focusAfter() {
|
||||
focus(this.$el, e => e.nextElementSibling);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -445,10 +293,6 @@ export default Vue.extend({
|
||||
&.reacted, &.reacted:hover
|
||||
color var(--noteActionsReactionHover)
|
||||
|
||||
> .detail
|
||||
padding-top 4px
|
||||
background rgba(#000, 0.0125)
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="stylus" module>
|
||||
|
@ -9,6 +9,12 @@
|
||||
<button @click="resolveInitPromise">%i18n:@retry%</button>
|
||||
</div>
|
||||
|
||||
<div class="skeleton" v-if="fetching">
|
||||
<template v-for="i in 10">
|
||||
<mk-note-skeleton :key="i"/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div" ref="notes">
|
||||
<template v-for="(note, i) in _notes">
|
||||
@ -226,6 +232,10 @@ export default Vue.extend({
|
||||
> *
|
||||
transition transform .3s ease, opacity .3s ease
|
||||
|
||||
> .skeleton
|
||||
padding 32px
|
||||
opacity 0.3
|
||||
|
||||
> .notes
|
||||
> .date
|
||||
display block
|
||||
|
@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div class="mk-notifications">
|
||||
<div class="skeleton" v-if="fetching">
|
||||
<template v-for="i in 10">
|
||||
<mk-note-skeleton :key="i"/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="notifications" v-if="notifications.length != 0">
|
||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div">
|
||||
@ -102,7 +108,6 @@
|
||||
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
|
||||
</button>
|
||||
<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
|
||||
<p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -202,6 +207,10 @@ export default Vue.extend({
|
||||
> *
|
||||
transition transform .3s ease, opacity .3s ease
|
||||
|
||||
> .skeleton
|
||||
padding 16px
|
||||
opacity 0.3
|
||||
|
||||
> .notifications
|
||||
> div
|
||||
> .notification
|
||||
@ -319,13 +328,4 @@ export default Vue.extend({
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> .loading
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
</style>
|
||||
|
@ -21,12 +21,13 @@
|
||||
<ui-button primary @click="save">%i18n:@save%</ui-button>
|
||||
<section>
|
||||
<h2>%i18n:@locked-account%</h2>
|
||||
<ui-switch v-model="$store.state.i.isLocked" @change="onChangeIsLocked">%i18n:@is-locked%</ui-switch>
|
||||
<ui-switch v-model="isLocked" @change="save(false)">%i18n:@is-locked%</ui-switch>
|
||||
<ui-switch v-model="carefulBot" @change="save(false)">%i18n:@careful-bot%</ui-switch>
|
||||
</section>
|
||||
<section>
|
||||
<h2>%i18n:@other%</h2>
|
||||
<ui-switch v-model="$store.state.i.isBot" @change="onChangeIsBot">%i18n:@is-bot%</ui-switch>
|
||||
<ui-switch v-model="$store.state.i.isCat" @change="onChangeIsCat">%i18n:@is-cat%</ui-switch>
|
||||
<ui-switch v-model="isBot" @change="save(false)">%i18n:@is-bot%</ui-switch>
|
||||
<ui-switch v-model="isCat" @change="save(false)">%i18n:@is-cat%</ui-switch>
|
||||
<ui-switch v-model="alwaysMarkNsfw">%i18n:common.always-mark-nsfw%</ui-switch>
|
||||
</section>
|
||||
</div>
|
||||
@ -42,6 +43,10 @@ export default Vue.extend({
|
||||
location: null,
|
||||
description: null,
|
||||
birthday: null,
|
||||
isBot: false,
|
||||
isCat: false,
|
||||
isLocked: false,
|
||||
carefulBot: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -55,34 +60,29 @@ export default Vue.extend({
|
||||
this.location = this.$store.state.i.profile.location;
|
||||
this.description = this.$store.state.i.description;
|
||||
this.birthday = this.$store.state.i.profile.birthday;
|
||||
this.isCat = this.$store.state.i.isCat;
|
||||
this.isBot = this.$store.state.i.isBot;
|
||||
this.isLocked = this.$store.state.i.isLocked;
|
||||
this.carefulBot = this.$store.state.i.carefulBot;
|
||||
},
|
||||
methods: {
|
||||
updateAvatar() {
|
||||
(this as any).apis.updateAvatar();
|
||||
},
|
||||
save() {
|
||||
save(notify) {
|
||||
(this as any).api('i/update', {
|
||||
name: this.name || null,
|
||||
location: this.location || null,
|
||||
description: this.description || null,
|
||||
birthday: this.birthday || null
|
||||
birthday: this.birthday || null,
|
||||
isCat: this.isCat,
|
||||
isBot: this.isBot,
|
||||
isLocked: this.isLocked,
|
||||
carefulBot: this.carefulBot
|
||||
}).then(() => {
|
||||
(this as any).apis.notify('%i18n:@profile-updated%');
|
||||
});
|
||||
},
|
||||
onChangeIsLocked() {
|
||||
(this as any).api('i/update', {
|
||||
isLocked: this.$store.state.i.isLocked
|
||||
});
|
||||
},
|
||||
onChangeIsBot() {
|
||||
(this as any).api('i/update', {
|
||||
isBot: this.$store.state.i.isBot
|
||||
});
|
||||
},
|
||||
onChangeIsCat() {
|
||||
(this as any).api('i/update', {
|
||||
isCat: this.$store.state.i.isCat
|
||||
if (notify) {
|
||||
(this as any).apis.notify('%i18n:@profile-updated%');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div class="mk-timeline-core">
|
||||
<mk-friends-maker v-if="src == 'home' && alone"/>
|
||||
<div class="fetching" v-if="fetching">
|
||||
<mk-ellipsis-icon/>
|
||||
</div>
|
||||
|
||||
<mk-notes ref="timeline" :more="existMore ? more : null">
|
||||
<p :class="$style.empty" slot="empty">
|
||||
@ -170,15 +167,10 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
|
||||
.mk-timeline-core
|
||||
> .mk-friends-maker
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
> .fetching
|
||||
padding 64px 0
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="stylus" module>
|
||||
|
@ -19,7 +19,7 @@
|
||||
<li @click="list">
|
||||
<p>%fa:list%<span>%i18n:@lists%</span>%fa:angle-right%</p>
|
||||
</li>
|
||||
<li @click="followRequests" v-if="$store.state.i.isLocked">
|
||||
<li @click="followRequests" v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
|
||||
<p>%fa:envelope R%<span>%i18n:@follow-requests%<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span>%fa:angle-right%</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -17,8 +17,6 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
|
||||
.note
|
||||
display inline-block
|
||||
padding 8px
|
||||
|
@ -78,7 +78,7 @@ export default Vue.extend({
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
this.connection.send('requestLog', {
|
||||
id: Math.random().toString(),
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 200
|
||||
});
|
||||
},
|
||||
|
@ -310,7 +310,7 @@ export default Vue.extend({
|
||||
|
||||
> header
|
||||
display flex
|
||||
z-index 1
|
||||
z-index 2
|
||||
line-height $header-height
|
||||
padding 0 16px
|
||||
font-size 14px
|
||||
|
@ -1,7 +1,15 @@
|
||||
<template>
|
||||
<div v-if="!mediaView" class="zyjjkidcqjnlegkqebitfviomuqmseqk" :class="{ renote: isRenote }">
|
||||
<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
|
||||
<x-sub :note="p.reply"/>
|
||||
<div
|
||||
v-if="!mediaView"
|
||||
v-show="appearNote.deletedAt == null"
|
||||
:tabindex="appearNote.deletedAt == null ? '-1' : null"
|
||||
class="zyjjkidcqjnlegkqebitfviomuqmseqk"
|
||||
:class="{ renote: isRenote }"
|
||||
v-hotkey="keymap"
|
||||
:title="title"
|
||||
>
|
||||
<div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
|
||||
<x-sub :note="appearNote.reply"/>
|
||||
</div>
|
||||
<div class="renote" v-if="isRenote">
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
@ -12,43 +20,42 @@
|
||||
<mk-time :time="note.createdAt"/>
|
||||
</div>
|
||||
<article>
|
||||
<mk-avatar class="avatar" :user="p.user"/>
|
||||
<mk-avatar class="avatar" :user="appearNote.user"/>
|
||||
<div class="main">
|
||||
<mk-note-header class="header" :note="p" :mini="true"/>
|
||||
<mk-note-header class="header" :note="appearNote" :mini="true"/>
|
||||
<div class="body">
|
||||
<p v-if="p.cw != null" class="cw">
|
||||
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
|
||||
<p v-if="appearNote.cw != null" class="cw">
|
||||
<span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span>
|
||||
<mk-cw-button v-model="showContent"/>
|
||||
</p>
|
||||
<div class="content" v-show="p.cw == null || showContent">
|
||||
<div class="content" v-show="appearNote.cw == null || showContent">
|
||||
<div class="text">
|
||||
<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
|
||||
<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
|
||||
<a class="reply" v-if="p.reply">%fa:reply%</a>
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
|
||||
<a class="rp" v-if="p.renote != null">RP:</a>
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
|
||||
<a class="reply" v-if="appearNote.reply">%fa:reply%</a>
|
||||
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i"/>
|
||||
<a class="rp" v-if="appearNote.renote != null">RP:</a>
|
||||
</div>
|
||||
<div class="files" v-if="p.files.length > 0">
|
||||
<mk-media-list :media-list="p.files"/>
|
||||
<div class="files" v-if="appearNote.files.length > 0">
|
||||
<mk-media-list :media-list="appearNote.files"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
||||
<div class="renote" v-if="p.renote">
|
||||
<mk-note-preview :note="p.renote" :mini="true"/>
|
||||
<mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
|
||||
<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
||||
<div class="renote" v-if="appearNote.renote">
|
||||
<mk-note-preview :note="appearNote.renote" :mini="true"/>
|
||||
</div>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="false" :mini="true"/>
|
||||
</div>
|
||||
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
|
||||
<span class="app" v-if="appearNote.app">via <b>{{ appearNote.app.name }}</b></span>
|
||||
</div>
|
||||
<footer>
|
||||
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
|
||||
<button @click="reply">
|
||||
<template v-if="p.reply">%fa:reply-all%</template>
|
||||
<mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
|
||||
<button @click="reply()">
|
||||
<template v-if="appearNote.reply">%fa:reply-all%</template>
|
||||
<template v-else>%fa:reply%</template>
|
||||
</button>
|
||||
<button @click="renote" title="Renote">%fa:retweet%</button>
|
||||
<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton">%fa:plus%</button>
|
||||
<button class="menu" @click="menu" ref="menuButton">%fa:ellipsis-h%</button>
|
||||
<button @click="renote()" title="Renote">%fa:retweet%</button>
|
||||
<button :class="{ reacted: appearNote.myReaction != null }" @click="react()" ref="reactButton">%fa:plus%</button>
|
||||
<button class="menu" @click="menu()" ref="menuButton">%fa:ellipsis-h%</button>
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
@ -65,11 +72,10 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import parse from '../../../../../../mfm/parse';
|
||||
|
||||
import MkNoteMenu from '../../../../common/views/components/note-menu.vue';
|
||||
import MkReactionPicker from '../../../../common/views/components/reaction-picker.vue';
|
||||
import MkPostFormWindow from '../../components/post-form-window.vue';
|
||||
import MkRenoteFormWindow from '../../components/renote-form-window.vue';
|
||||
import XSub from './deck.note.sub.vue';
|
||||
import noteMixin from '../../../../common/scripts/note-mixin';
|
||||
import noteSubscriber from '../../../../common/scripts/note-subscriber';
|
||||
|
||||
export default Vue.extend({
|
||||
@ -77,7 +83,10 @@ export default Vue.extend({
|
||||
XSub
|
||||
},
|
||||
|
||||
mixins: [noteSubscriber('note')],
|
||||
mixins: [
|
||||
noteMixin(),
|
||||
noteSubscriber('note')
|
||||
],
|
||||
|
||||
props: {
|
||||
note: {
|
||||
@ -89,66 +98,6 @@ export default Vue.extend({
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showContent: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isRenote(): boolean {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
this.note.fileIds.length == 0 &&
|
||||
this.note.poll == null);
|
||||
},
|
||||
|
||||
p(): any {
|
||||
return this.isRenote ? this.note.renote : this.note;
|
||||
},
|
||||
|
||||
urls(): string[] {
|
||||
if (this.p.text) {
|
||||
const ast = parse(this.p.text);
|
||||
return ast
|
||||
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
|
||||
.map(t => t.url);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
reply() {
|
||||
(this as any).apis.post({
|
||||
reply: this.p
|
||||
});
|
||||
},
|
||||
|
||||
renote() {
|
||||
(this as any).apis.post({
|
||||
renote: this.p
|
||||
});
|
||||
},
|
||||
|
||||
react() {
|
||||
(this as any).os.new(MkReactionPicker, {
|
||||
source: this.$refs.reactButton,
|
||||
note: this.p,
|
||||
compact: true
|
||||
});
|
||||
},
|
||||
|
||||
menu() {
|
||||
(this as any).os.new(MkNoteMenu, {
|
||||
source: this.$refs.menuButton,
|
||||
note: this.p,
|
||||
compact: true
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -168,6 +117,20 @@ export default Vue.extend({
|
||||
font-size 13px
|
||||
border-bottom solid 1px var(--faceDivider)
|
||||
|
||||
&:focus
|
||||
z-index 1
|
||||
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top 2px
|
||||
right 2px
|
||||
bottom 2px
|
||||
left 2px
|
||||
border 2px solid var(--primaryAlpha03)
|
||||
border-radius 4px
|
||||
|
||||
&:last-of-type
|
||||
border-bottom none
|
||||
|
||||
|
@ -3,9 +3,6 @@
|
||||
<header :class="$style.header">
|
||||
<h1>{{ q }}</h1>
|
||||
</header>
|
||||
<div :class="$style.loading" v-if="fetching">
|
||||
<mk-ellipsis-icon/>
|
||||
</div>
|
||||
<p :class="$style.notAvailable" v-if="!fetching && notAvailable">%i18n:@not-available%</p>
|
||||
<p :class="$style.empty" v-if="!fetching && empty">%fa:search% {{ '%i18n:not-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:not-found%'.split('{}')[1] }}</p>
|
||||
<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
|
||||
@ -119,9 +116,6 @@ export default Vue.extend({
|
||||
border-radius 6px
|
||||
overflow hidden
|
||||
|
||||
.loading
|
||||
padding 64px 0
|
||||
|
||||
.empty
|
||||
display block
|
||||
margin 0 auto
|
||||
|
@ -3,9 +3,6 @@
|
||||
<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% {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p>
|
||||
<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
|
||||
</mk-ui>
|
||||
@ -108,9 +105,6 @@ export default Vue.extend({
|
||||
border-radius 6px
|
||||
overflow hidden
|
||||
|
||||
.loading
|
||||
padding 64px 0
|
||||
|
||||
.empty
|
||||
display block
|
||||
margin 0 auto
|
||||
|
@ -8,8 +8,6 @@
|
||||
<div>
|
||||
<span class="username"><mk-acct :user="user" :detail="true" /></span>
|
||||
<span v-if="user.isBot" title="%i18n:@is-bot%">%fa:robot%</span>
|
||||
<span class="location" v-if="user.host === null && user.profile.location">%fa:map-marker% {{ user.profile.location }}</span>
|
||||
<span class="birthday" v-if="user.host === null && user.profile.birthday">%fa:birthday-cake% {{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -18,6 +16,10 @@
|
||||
<div class="description">
|
||||
<misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="location" v-if="user.host === null && user.profile.location">%fa:map-marker% {{ user.profile.location }}</span>
|
||||
<span class="birthday" v-if="user.host === null && user.profile.birthday">%fa:birthday-cake% {{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</span>
|
||||
</div>
|
||||
<div class="status">
|
||||
<span class="notes-count"><b>{{ user.notesCount | number }}</b>%i18n:@posts%</span>
|
||||
<span class="following clickable" @click="showFollowing"><b>{{ user.followingCount | number }}</b>%i18n:@following%</span>
|
||||
@ -182,6 +184,14 @@ export default Vue.extend({
|
||||
padding 16px 16px 16px 154px
|
||||
color var(--text)
|
||||
|
||||
> .info
|
||||
margin-top 16px
|
||||
padding-top 16px
|
||||
border-top solid 1px var(--faceDivider)
|
||||
|
||||
> *
|
||||
margin-right 16px
|
||||
|
||||
> .status
|
||||
margin-top 16px
|
||||
padding-top 16px
|
||||
|
@ -5,9 +5,6 @@
|
||||
<span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">%fa:comments% %i18n:@with-replies%</span>
|
||||
<span :data-active="mode == 'with-media'" @click="mode = 'with-media'">%fa:images% %i18n:@with-media%</span>
|
||||
</header>
|
||||
<div class="loading" v-if="fetching">
|
||||
<mk-ellipsis-icon/>
|
||||
</div>
|
||||
<mk-notes ref="timeline" :more="existMore ? more : null">
|
||||
<p class="empty" slot="empty">%fa:R comments%%i18n:@empty%</p>
|
||||
</mk-notes>
|
||||
@ -152,9 +149,6 @@ export default Vue.extend({
|
||||
&:hover
|
||||
color var(--desktopTimelineSrcHover)
|
||||
|
||||
> .loading
|
||||
padding 64px 0
|
||||
|
||||
> .empty
|
||||
display block
|
||||
margin 0 auto
|
||||
|
@ -443,10 +443,10 @@ export default class MiOS extends EventEmitter {
|
||||
};
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const viaStream = this.stream && this.store.state.device.apiViaStream && !forceFetch;
|
||||
const viaStream = this.stream && this.stream.state == 'connected' && this.store.state.device.apiViaStream && !forceFetch;
|
||||
|
||||
if (viaStream) {
|
||||
const id = Math.random().toString();
|
||||
const id = Math.random().toString().substr(2, 8);
|
||||
|
||||
this.stream.once(`api:${id}`, res => {
|
||||
if (res == null || Object.keys(res).length == 0) {
|
||||
|
@ -18,6 +18,7 @@ export default (os) => (opts) => {
|
||||
}).$mount();
|
||||
vm.$once('cancel', recover);
|
||||
vm.$once('posted', recover);
|
||||
if (o.cb) vm.$once('closed', o.cb);
|
||||
document.body.appendChild(vm.$el);
|
||||
(vm as any).focus();
|
||||
};
|
||||
|
@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<div class="note" :class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart' }">
|
||||
<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
|
||||
<x-sub :note="p.reply"/>
|
||||
<div
|
||||
class="note"
|
||||
v-show="appearNote.deletedAt == null"
|
||||
:tabindex="appearNote.deletedAt == null ? '-1' : null"
|
||||
:class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart' }"
|
||||
v-hotkey="keymap"
|
||||
>
|
||||
<div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
|
||||
<x-sub :note="appearNote.reply"/>
|
||||
</div>
|
||||
<div class="renote" v-if="isRenote">
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
@ -12,47 +18,45 @@
|
||||
<mk-time :time="note.createdAt"/>
|
||||
</div>
|
||||
<article>
|
||||
<mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle != 'smart'"/>
|
||||
<mk-avatar class="avatar" :user="appearNote.user" v-if="$store.state.device.postStyle != 'smart'"/>
|
||||
<div class="main">
|
||||
<mk-note-header class="header" :note="p" :mini="true"/>
|
||||
<mk-note-header class="header" :note="appearNote" :mini="true"/>
|
||||
<div class="body">
|
||||
<p v-if="p.cw != null" class="cw">
|
||||
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
|
||||
<p v-if="appearNote.cw != null" class="cw">
|
||||
<span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span>
|
||||
<mk-cw-button v-model="showContent"/>
|
||||
</p>
|
||||
<div class="content" v-show="p.cw == null || showContent">
|
||||
<div class="content" v-show="appearNote.cw == null || showContent">
|
||||
<div class="text">
|
||||
<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
|
||||
<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
|
||||
<a class="reply" v-if="p.reply">%fa:reply%</a>
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/>
|
||||
<a class="rp" v-if="p.renote != null">RP:</a>
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
|
||||
<a class="reply" v-if="appearNote.reply">%fa:reply%</a>
|
||||
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text"/>
|
||||
<a class="rp" v-if="appearNote.renote != null">RP:</a>
|
||||
</div>
|
||||
<div class="files" v-if="p.files.length > 0">
|
||||
<mk-media-list :media-list="p.files"/>
|
||||
<div class="files" v-if="appearNote.files.length > 0">
|
||||
<mk-media-list :media-list="appearNote.files"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||
<mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
||||
<a class="location" v-if="p.geo" :href="`https://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="renote" v-if="p.renote"><mk-note-preview :note="p.renote"/></div>
|
||||
<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
||||
<div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div>
|
||||
</div>
|
||||
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
|
||||
<span class="app" v-if="appearNote.app">via <b>{{ appearNote.app.name }}</b></span>
|
||||
</div>
|
||||
<footer v-if="p.deletedAt == null">
|
||||
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
|
||||
<button @click="reply">
|
||||
<template v-if="p.reply">%fa:reply-all%</template>
|
||||
<footer>
|
||||
<mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
|
||||
<button @click="reply()">
|
||||
<template v-if="appearNote.reply">%fa:reply-all%</template>
|
||||
<template v-else>%fa:reply%</template>
|
||||
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
|
||||
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<button @click="renote" title="Renote">
|
||||
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
|
||||
<button @click="renote()" title="Renote">
|
||||
%fa:retweet%<p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
|
||||
</button>
|
||||
<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton">
|
||||
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
|
||||
<button :class="{ reacted: appearNote.myReaction != null }" @click="react()" ref="reactButton">
|
||||
%fa:plus%<p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p>
|
||||
</button>
|
||||
<button class="menu" @click="menu" ref="menuButton">
|
||||
<button class="menu" @click="menu()" ref="menuButton">
|
||||
%fa:ellipsis-h%
|
||||
</button>
|
||||
</footer>
|
||||
@ -63,12 +67,9 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import parse from '../../../../../mfm/parse';
|
||||
|
||||
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
|
||||
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
|
||||
import XSub from './note.sub.vue';
|
||||
import { sum } from '../../../../../prelude/array';
|
||||
import noteMixin from '../../../common/scripts/note-mixin';
|
||||
import noteSubscriber from '../../../common/scripts/note-subscriber';
|
||||
|
||||
export default Vue.extend({
|
||||
@ -76,74 +77,17 @@ export default Vue.extend({
|
||||
XSub
|
||||
},
|
||||
|
||||
mixins: [noteSubscriber('note')],
|
||||
mixins: [
|
||||
noteMixin({
|
||||
mobile: true
|
||||
}),
|
||||
noteSubscriber('note')
|
||||
],
|
||||
|
||||
props: ['note'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
showContent: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isRenote(): boolean {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
this.note.fileIds.length == 0 &&
|
||||
this.note.poll == null);
|
||||
},
|
||||
|
||||
p(): any {
|
||||
return this.isRenote ? this.note.renote : this.note;
|
||||
},
|
||||
|
||||
reactionsCount(): number {
|
||||
return this.p.reactionCounts
|
||||
? sum(Object.values(this.p.reactionCounts))
|
||||
: 0;
|
||||
},
|
||||
|
||||
urls(): string[] {
|
||||
if (this.p.text) {
|
||||
const ast = parse(this.p.text);
|
||||
return ast
|
||||
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
|
||||
.map(t => t.url);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
reply() {
|
||||
(this as any).apis.post({
|
||||
reply: this.p
|
||||
});
|
||||
},
|
||||
|
||||
renote() {
|
||||
(this as any).apis.post({
|
||||
renote: this.p
|
||||
});
|
||||
},
|
||||
|
||||
react() {
|
||||
(this as any).os.new(MkReactionPicker, {
|
||||
source: this.$refs.reactButton,
|
||||
note: this.p,
|
||||
compact: true,
|
||||
big: true
|
||||
});
|
||||
},
|
||||
|
||||
menu() {
|
||||
(this as any).os.new(MkNoteMenu, {
|
||||
source: this.$refs.menuButton,
|
||||
note: this.p,
|
||||
compact: true
|
||||
});
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -154,6 +98,20 @@ export default Vue.extend({
|
||||
font-size 12px
|
||||
border-bottom solid 1px var(--faceDivider)
|
||||
|
||||
&:focus
|
||||
z-index 1
|
||||
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top 2px
|
||||
right 2px
|
||||
bottom 2px
|
||||
left 2px
|
||||
border 2px solid var(--primaryAlpha03)
|
||||
border-radius 4px
|
||||
|
||||
&:last-of-type
|
||||
border-bottom none
|
||||
|
||||
|
@ -4,8 +4,10 @@
|
||||
|
||||
<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
|
||||
|
||||
<div class="init" v-if="fetching">
|
||||
%fa:spinner .pulse%%i18n:common.loading%
|
||||
<div class="skeleton" v-if="fetching">
|
||||
<template v-for="i in 10">
|
||||
<mk-note-skeleton :key="i"/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="!fetching && requestInitPromise != null">
|
||||
@ -251,13 +253,12 @@ export default Vue.extend({
|
||||
[data-fa]
|
||||
margin-right 8px
|
||||
|
||||
> .init
|
||||
padding 64px 0
|
||||
text-align center
|
||||
color #999
|
||||
> .skeleton
|
||||
padding 16px
|
||||
opacity 0.3
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
@media (min-width 500px)
|
||||
padding 32px
|
||||
|
||||
> .empty
|
||||
margin 0 auto
|
||||
|
@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div class="mk-notifications">
|
||||
<div class="skeleton" v-if="fetching">
|
||||
<template v-for="i in 10">
|
||||
<mk-note-skeleton :key="i"/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications">
|
||||
<template v-for="(notification, i) in _notifications">
|
||||
@ -17,7 +23,6 @@
|
||||
</button>
|
||||
|
||||
<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
|
||||
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -179,13 +184,11 @@ export default Vue.extend({
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> .fetching
|
||||
margin 0
|
||||
> .skeleton
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
opacity 0.3
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
@media (min-width 500px)
|
||||
padding 32px
|
||||
|
||||
</style>
|
||||
|
@ -18,7 +18,7 @@
|
||||
<li><router-link to="/" :data-active="$route.name == 'index'">%fa:home%%i18n:@timeline%%fa:angle-right%</router-link></li>
|
||||
<li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'">%fa:R bell%%i18n:@notifications%<template v-if="hasUnreadNotification">%fa:circle%</template>%fa:angle-right%</router-link></li>
|
||||
<li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'">%fa:R comments%%i18n:@messaging%<template v-if="hasUnreadMessagingMessage">%fa:circle%</template>%fa:angle-right%</router-link></li>
|
||||
<li v-if="$store.getters.isSignedIn && $store.state.i.isLocked"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'">%fa:R envelope%%i18n:@follow-requests%<template v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount">%fa:circle%</template>%fa:angle-right%</router-link></li>
|
||||
<li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'">%fa:R envelope%%i18n:@follow-requests%<template v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount">%fa:circle%</template>%fa:angle-right%</router-link></li>
|
||||
<li><router-link to="/reversi" :data-active="$route.name == 'reversi'">%fa:gamepad%%i18n:@game%<template v-if="hasGameInvitation">%fa:circle%</template>%fa:angle-right%</router-link></li>
|
||||
</ul>
|
||||
<ul>
|
||||
|
@ -8,7 +8,14 @@
|
||||
<x-profile/>
|
||||
|
||||
<ui-card>
|
||||
<div slot="title">%fa:palette% %i18n:@design%</div>
|
||||
<div slot="title">%fa:palette% %i18n:@theme%</div>
|
||||
<section>
|
||||
<mk-theme/>
|
||||
</section>
|
||||
</ui-card>
|
||||
|
||||
<ui-card>
|
||||
<div slot="title">%fa:poll-h% %i18n:@design%</div>
|
||||
|
||||
<section>
|
||||
<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
|
||||
@ -23,13 +30,6 @@
|
||||
<ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>%i18n:@theme%</header>
|
||||
<div>
|
||||
<mk-theme/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>%i18n:@timeline%</header>
|
||||
<div>
|
||||
@ -54,7 +54,7 @@
|
||||
</ui-card>
|
||||
|
||||
<ui-card>
|
||||
<div slot="title">%fa:cog% %i18n:@behavior%</div>
|
||||
<div slot="title">%fa:sliders-h% %i18n:@behavior%</div>
|
||||
|
||||
<section>
|
||||
<ui-switch v-model="fetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch>
|
||||
|
@ -58,6 +58,7 @@
|
||||
|
||||
<div>
|
||||
<ui-switch v-model="isLocked" @change="save(false)">%i18n:@is-locked%</ui-switch>
|
||||
<ui-switch v-model="carefulBot" @change="save(false)">%i18n:@careful-bot%</ui-switch>
|
||||
</div>
|
||||
</section>
|
||||
</ui-card>
|
||||
@ -80,6 +81,7 @@ export default Vue.extend({
|
||||
bannerId: null,
|
||||
isCat: false,
|
||||
isLocked: false,
|
||||
carefulBot: false,
|
||||
saving: false,
|
||||
avatarUploading: false,
|
||||
bannerUploading: false
|
||||
@ -103,6 +105,7 @@ export default Vue.extend({
|
||||
this.bannerId = this.$store.state.i.bannerId;
|
||||
this.isCat = this.$store.state.i.isCat;
|
||||
this.isLocked = this.$store.state.i.isLocked;
|
||||
this.carefulBot = this.$store.state.i.carefulBot;
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -161,7 +164,8 @@ export default Vue.extend({
|
||||
avatarId: this.avatarId,
|
||||
bannerId: this.bannerId,
|
||||
isCat: this.isCat,
|
||||
isLocked: this.isLocked
|
||||
isLocked: this.isLocked,
|
||||
carefulBot: this.carefulBot
|
||||
}).then(i => {
|
||||
this.saving = false;
|
||||
this.$store.state.i.avatarId = i.avatarId;
|
||||
|
@ -62,6 +62,8 @@ export type Source = {
|
||||
*/
|
||||
ghost?: string;
|
||||
|
||||
proxy?: string;
|
||||
|
||||
summalyProxy?: string;
|
||||
|
||||
accesslog?: string;
|
||||
@ -93,11 +95,9 @@ export type Source = {
|
||||
private_key: string;
|
||||
};
|
||||
|
||||
google_maps_api_key: string;
|
||||
|
||||
clusterLimit?: number;
|
||||
|
||||
user_recommendation: {
|
||||
user_recommendation?: {
|
||||
external: boolean;
|
||||
engine: string;
|
||||
timeout: number;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import * as redis from 'redis';
|
||||
import config from '../config';
|
||||
|
||||
export default redis.createClient(
|
||||
export default config.redis ? redis.createClient(
|
||||
config.redis.port,
|
||||
config.redis.host,
|
||||
{
|
||||
auth_pass: config.redis.pass
|
||||
}
|
||||
);
|
||||
) : null;
|
||||
|
@ -30,6 +30,8 @@
|
||||
<tr><td><kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">→</kbd></kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">Q</kbd></kbd></td><td>即刻Renoteする(フォームを開かずに)</td><td>-</td></tr>
|
||||
<tr><td><kbd class="key">E</kbd>, <kbd class="key">A</kbd>, <kbd class="key">+</kbd></td><td>リアクションフォームを開く</td><td><b>E</b>mote, re<b>A</b>ction</td></tr>
|
||||
<tr><td><kbd class="key">0</kbd>~<kbd class="key">9</kbd></td><td>数字に対応したリアクションをする(対応については後述)</td><td>-</td></tr>
|
||||
<tr><td><kbd class="key">F</kbd>, <kbd class="key">B</kbd></td><td>お気に入りに登録</td><td><b>F</b>avorite, <b>B</b>ookmark</td></tr>
|
||||
<tr><td><kbd class="key">Del</kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">D</kbd></kbd></td><td>投稿を削除</td><td><b>D</b>elete</tr>
|
||||
<tr><td><kbd class="key">M</kbd>, <kbd class="key">O</kbd></td><td>投稿に対するメニューを開く</td><td><b>M</b>ore, <b>O</b>ther</td></tr>
|
||||
<tr><td><kbd class="key">S</kbd></td><td>CWで隠された部分を表示 or 隠す</td><td><b>S</b>how, <b>S</b>ee</td></tr>
|
||||
<tr><td><kbd class="key">Esc</kbd></td><td>フォーカスを外す</td><td>-</td></tr>
|
||||
|
@ -11,6 +11,8 @@ import DriveFileThumbnail, { deleteDriveFileThumbnail } from './drive-file-thumb
|
||||
const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
|
||||
DriveFile.createIndex('md5');
|
||||
DriveFile.createIndex('metadata.uri');
|
||||
DriveFile.createIndex('metadata.userId');
|
||||
DriveFile.createIndex('metadata.folderId');
|
||||
export default DriveFile;
|
||||
|
||||
export const DriveFileChunk = monkDb.get('driveFiles.chunks');
|
||||
@ -166,7 +168,7 @@ export const pack = (
|
||||
|
||||
// (データベースの欠損などで)ファイルがデータベース上に見つからなかったとき
|
||||
if (_file == null) {
|
||||
console.warn(`in packaging driveFile: driveFile not found on database: ${_file}`);
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: driveFile :: ${file}`);
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import db from '../db/mongodb';
|
||||
import DriveFile from './drive-file';
|
||||
|
||||
const DriveFolder = db.get<IDriveFolder>('driveFolders');
|
||||
DriveFolder.createIndex('userId');
|
||||
export default DriveFolder;
|
||||
|
||||
export type IDriveFolder = {
|
||||
|
@ -75,11 +75,13 @@ export const pack = (
|
||||
delete _favorite._id;
|
||||
|
||||
// Populate note
|
||||
_favorite.note = await packNote(_favorite.noteId, me);
|
||||
_favorite.note = await packNote(_favorite.noteId, me, {
|
||||
detail: true
|
||||
});
|
||||
|
||||
// (データベースの不具合などで)投稿が見つからなかったら
|
||||
if (_favorite.note == null) {
|
||||
console.warn(`in packaging favorite: note not found on database: ${_favorite.noteId}`);
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: favorite -> note :: ${_favorite.id} (note ${_favorite.noteId})`);
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
|
@ -281,9 +281,9 @@ export const pack = async (
|
||||
_note = deepcopy(note);
|
||||
}
|
||||
|
||||
// 投稿がデータベース上に見つからなかったとき
|
||||
// (データベースの欠損などで)投稿がデータベース上に見つからなかったとき
|
||||
if (_note == null) {
|
||||
console.warn(`note not found on database: ${note}`);
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: note :: ${note}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -358,8 +358,8 @@ export const pack = async (
|
||||
})(_note.poll);
|
||||
}
|
||||
|
||||
// Fetch my reaction
|
||||
if (meId) {
|
||||
// Fetch my reaction
|
||||
_note.myReaction = (async () => {
|
||||
const reaction = await Reaction
|
||||
.findOne({
|
||||
@ -374,18 +374,44 @@ export const pack = async (
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
// isFavorited
|
||||
_note.isFavorited = (async () => {
|
||||
const favorite = await Favorite
|
||||
.count({
|
||||
userId: meId,
|
||||
noteId: id
|
||||
}, {
|
||||
limit: 1
|
||||
});
|
||||
|
||||
return favorite === 1;
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
// resolve promises in _note object
|
||||
_note = await rap(_note);
|
||||
|
||||
// (データベースの欠損などで)ユーザーがデータベース上に見つからなかったとき
|
||||
//#region (データベースの欠損などで)参照しているデータがデータベース上に見つからなかったとき
|
||||
if (_note.user == null) {
|
||||
console.warn(`in packaging note: note user not found on database: note(${_note.id})`);
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: note -> user :: ${_note.id} (user ${_note.userId})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (opts.detail) {
|
||||
if (_note.replyId != null && _note.reply == null) {
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: note -> reply :: ${_note.id} (reply ${_note.replyId})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_note.renoteId != null && _note.renote == null) {
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: note -> renote :: ${_note.id} (renote ${_note.renoteId})`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
if (_note.user.isCat && _note.text) {
|
||||
_note.text = _note.text.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ');
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ export const pack = (notification: any) => new Promise<any>(async (resolve, reje
|
||||
|
||||
// (データベースの不具合などで)投稿が見つからなかったら
|
||||
if (_notification.note == null) {
|
||||
console.warn(`in packaging notification: note not found on database: ${_notification.noteId}`);
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: notification -> note :: ${_notification.id} (note ${_notification.noteId})`);
|
||||
return resolve(null);
|
||||
}
|
||||
break;
|
||||
|
@ -65,6 +65,16 @@ type IUserBase = {
|
||||
*/
|
||||
isLocked: boolean;
|
||||
|
||||
/**
|
||||
* Botか否か
|
||||
*/
|
||||
isBot: boolean;
|
||||
|
||||
/**
|
||||
* Botからのフォローを承認制にするか
|
||||
*/
|
||||
carefulBot: boolean;
|
||||
|
||||
/**
|
||||
* このアカウントに届いているフォローリクエストの数
|
||||
*/
|
||||
@ -94,7 +104,6 @@ export interface ILocalUser extends IUserBase {
|
||||
tags: string[];
|
||||
};
|
||||
lastUsedAt: Date;
|
||||
isBot: boolean;
|
||||
isCat: boolean;
|
||||
isAdmin?: boolean;
|
||||
isVerified?: boolean;
|
||||
|
@ -2,7 +2,7 @@ import * as debug from 'debug';
|
||||
|
||||
import uploadFromUrl from '../../../services/drive/upload-from-url';
|
||||
import { IRemoteUser } from '../../../models/user';
|
||||
import { IDriveFile } from '../../../models/drive-file';
|
||||
import DriveFile, { IDriveFile } from '../../../models/drive-file';
|
||||
import Resolver from '../resolver';
|
||||
|
||||
const log = debug('misskey:activitypub');
|
||||
@ -24,7 +24,22 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv
|
||||
|
||||
log(`Creating the Image: ${image.url}`);
|
||||
|
||||
return await uploadFromUrl(image.url, actor, null, image.url, image.sensitive);
|
||||
let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive);
|
||||
|
||||
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
|
||||
// URLを更新する
|
||||
if (file.metadata.url !== image.url) {
|
||||
file = await DriveFile.findOneAndUpdate({ _id: file._id }, {
|
||||
$set: {
|
||||
'metadata.url': image.url,
|
||||
'metadata.uri': image.url
|
||||
}
|
||||
}, {
|
||||
returnNewDocument: true
|
||||
});
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,6 +51,7 @@ export default class Resolver {
|
||||
|
||||
const object = await request({
|
||||
url: value,
|
||||
proxy: config.proxy,
|
||||
timeout: this.timeout,
|
||||
headers: {
|
||||
'User-Agent': config.user_agent,
|
||||
|
@ -27,7 +27,8 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
|
||||
|
||||
const file = await DriveFile.findOne({
|
||||
md5: md5,
|
||||
'metadata.userId': user._id
|
||||
'metadata.userId': user._id,
|
||||
'metadata.deletedAt': { $exists: false }
|
||||
});
|
||||
|
||||
if (file === null) {
|
||||
|
@ -22,7 +22,8 @@ export default async (params: any, user: ILocalUser) => {
|
||||
const file = await DriveFile
|
||||
.findOne({
|
||||
_id: fileId,
|
||||
'metadata.userId': user._id
|
||||
'metadata.userId': user._id,
|
||||
'metadata.deletedAt': { $exists: false }
|
||||
});
|
||||
|
||||
if (file === null) {
|
||||
|
@ -67,6 +67,12 @@ export const meta = {
|
||||
}
|
||||
}),
|
||||
|
||||
carefulBot: $.bool.optional.note({
|
||||
desc: {
|
||||
'ja-JP': 'Botからのフォローを承認制にするか'
|
||||
}
|
||||
}),
|
||||
|
||||
isBot: $.bool.optional.note({
|
||||
desc: {
|
||||
'ja-JP': 'Botか否か'
|
||||
@ -110,6 +116,7 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a
|
||||
if (ps.wallpaperId !== undefined) updates.wallpaperId = ps.wallpaperId;
|
||||
if (typeof ps.isLocked == 'boolean') updates.isLocked = ps.isLocked;
|
||||
if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot;
|
||||
if (typeof ps.carefulBot == 'boolean') updates.carefulBot = ps.carefulBot;
|
||||
if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat;
|
||||
if (typeof ps.autoWatch == 'boolean') updates['settings.autoWatch'] = ps.autoWatch;
|
||||
if (typeof ps.alwaysMarkNsfw == 'boolean') updates['settings.alwaysMarkNsfw'] = ps.alwaysMarkNsfw;
|
||||
|
@ -58,6 +58,8 @@ export default async (params: any, user: ILocalUser) => {
|
||||
};
|
||||
|
||||
const query = {
|
||||
deletedAt: null,
|
||||
|
||||
// public only
|
||||
visibility: 'public',
|
||||
|
||||
|
@ -129,6 +129,8 @@ export default async (params: any, user: ILocalUser) => {
|
||||
|
||||
const query = {
|
||||
$and: [{
|
||||
deletedAt: null,
|
||||
|
||||
$or: [{
|
||||
// フォローしている人の投稿
|
||||
$or: followQuery
|
||||
|
@ -71,6 +71,8 @@ export default async (params: any, user: ILocalUser) => {
|
||||
};
|
||||
|
||||
const query = {
|
||||
deletedAt: null,
|
||||
|
||||
// public only
|
||||
visibility: 'public',
|
||||
|
||||
|
@ -45,6 +45,8 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
|
||||
|
||||
// Construct query
|
||||
const query = {
|
||||
deletedAt: null,
|
||||
|
||||
$or: [{
|
||||
mentions: user._id
|
||||
}, {
|
||||
|
@ -132,6 +132,8 @@ export default async (params: any, user: ILocalUser) => {
|
||||
|
||||
const query = {
|
||||
$and: [{
|
||||
deletedAt: null,
|
||||
|
||||
// フォローしている人の投稿
|
||||
$or: followQuery,
|
||||
|
||||
|
@ -137,6 +137,8 @@ export default async (params: any, user: ILocalUser) => {
|
||||
|
||||
const query = {
|
||||
$and: [{
|
||||
deletedAt: null,
|
||||
|
||||
// リストに入っている人のタイムラインへの投稿
|
||||
$or: listQuery,
|
||||
|
||||
|
@ -136,6 +136,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
|
||||
};
|
||||
|
||||
const query = {
|
||||
deletedAt: null,
|
||||
userId: user._id
|
||||
} as any;
|
||||
|
||||
|
@ -30,22 +30,20 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
|
||||
.replace('{{limit}}', limit)
|
||||
.replace('{{offset}}', offset);
|
||||
|
||||
request(
|
||||
{
|
||||
url: url,
|
||||
timeout: timeout,
|
||||
json: true,
|
||||
followRedirect: true,
|
||||
followAllRedirects: true
|
||||
},
|
||||
(error: any, response: any, body: any) => {
|
||||
if (!error && response.statusCode == 200) {
|
||||
res(body);
|
||||
} else {
|
||||
res([]);
|
||||
}
|
||||
request({
|
||||
url: url,
|
||||
proxy: config.proxy,
|
||||
timeout: timeout,
|
||||
json: true,
|
||||
followRedirect: true,
|
||||
followAllRedirects: true
|
||||
}, (error: any, response: any, body: any) => {
|
||||
if (!error && response.statusCode == 200) {
|
||||
res(body);
|
||||
} else {
|
||||
res([]);
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Get 'limit' parameter
|
||||
const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
|
||||
@ -69,13 +67,10 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
|
||||
$nin: followingIds.concat(mutedUserIds)
|
||||
},
|
||||
isLocked: { $ne: true },
|
||||
$or: [{
|
||||
lastUsedAt: {
|
||||
$gte: new Date(Date.now() - ms('7days'))
|
||||
}
|
||||
}, {
|
||||
host: null
|
||||
}]
|
||||
lastUsedAt: {
|
||||
$gte: new Date(Date.now() - ms('7days'))
|
||||
},
|
||||
host: null
|
||||
}, {
|
||||
limit: limit,
|
||||
skip: offset,
|
||||
|
@ -8,6 +8,12 @@ import { IUser } from '../../models/user';
|
||||
const log = debug('misskey:limitter');
|
||||
|
||||
export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) => {
|
||||
// Redisがインストールされてない場合は常に許可
|
||||
if (limiterDB == null) {
|
||||
ok();
|
||||
return;
|
||||
}
|
||||
|
||||
const limitation = endpoint.meta.limit;
|
||||
|
||||
const key = limitation.hasOwnProperty('key')
|
||||
|
@ -63,6 +63,7 @@ handler.on('status', event => {
|
||||
// Fetch parent status
|
||||
request({
|
||||
url: `${parent.url}/statuses`,
|
||||
proxy: config.proxy,
|
||||
headers: {
|
||||
'User-Agent': 'misskey'
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ router.get('/disconnect/twitter', async ctx => {
|
||||
}));
|
||||
});
|
||||
|
||||
if (config.twitter == null) {
|
||||
if (config.twitter == null || redis == null) {
|
||||
router.get('/connect/twitter', ctx => {
|
||||
ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)';
|
||||
});
|
||||
|
@ -7,6 +7,8 @@ import Connection from '.';
|
||||
export default abstract class Channel {
|
||||
protected connection: Connection;
|
||||
public id: string;
|
||||
public abstract readonly chName: string;
|
||||
public static readonly shouldShare: boolean;
|
||||
|
||||
protected get user() {
|
||||
return this.connection.user;
|
||||
|
@ -2,6 +2,9 @@ import autobind from 'autobind-decorator';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'drive';
|
||||
public static shouldShare = true;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
// Subscribe drive stream
|
||||
|
@ -1,5 +1,6 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import * as CRC32 from 'crc-32';
|
||||
import * as mongo from 'mongodb';
|
||||
import ReversiGame, { pack } from '../../../../../models/games/reversi/game';
|
||||
import { publishReversiGameStream } from '../../../../../stream';
|
||||
import Reversi from '../../../../../games/reversi/core';
|
||||
@ -7,11 +8,14 @@ import * as maps from '../../../../../games/reversi/maps';
|
||||
import Channel from '../../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
private gameId: string;
|
||||
public readonly chName = 'gamesReversiGame';
|
||||
public static shouldShare = false;
|
||||
|
||||
private gameId: mongo.ObjectID;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
this.gameId = params.gameId as string;
|
||||
this.gameId = new mongo.ObjectID(params.gameId as string);
|
||||
|
||||
// Subscribe game stream
|
||||
this.subscriber.on(`reversiGameStream:${this.gameId}`, data => {
|
||||
|
@ -5,6 +5,9 @@ import { publishMainStream } from '../../../../../stream';
|
||||
import Channel from '../../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'gamesReversi';
|
||||
public static shouldShare = true;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
// Subscribe reversi stream
|
||||
|
@ -5,6 +5,9 @@ import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'globalTimeline';
|
||||
public static shouldShare = true;
|
||||
|
||||
private mutedUserIds: string[] = [];
|
||||
|
||||
@autobind
|
||||
|
@ -5,6 +5,9 @@ import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'hashtag';
|
||||
public static shouldShare = false;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null;
|
||||
|
@ -5,6 +5,9 @@ import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'homeTimeline';
|
||||
public static shouldShare = true;
|
||||
|
||||
private mutedUserIds: string[] = [];
|
||||
|
||||
@autobind
|
||||
|
@ -5,6 +5,9 @@ import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'hybridTimeline';
|
||||
public static shouldShare = true;
|
||||
|
||||
private mutedUserIds: string[] = [];
|
||||
|
||||
@autobind
|
||||
|
@ -5,6 +5,9 @@ import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'localTimeline';
|
||||
public static shouldShare = true;
|
||||
|
||||
private mutedUserIds: string[] = [];
|
||||
|
||||
@autobind
|
||||
|
@ -3,6 +3,9 @@ import Mute from '../../../../models/mute';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'main';
|
||||
public static shouldShare = true;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
const mute = await Mute.find({ muterId: this.user._id });
|
||||
|
@ -2,6 +2,9 @@ import autobind from 'autobind-decorator';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'messagingIndex';
|
||||
public static shouldShare = true;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
// Subscribe messaging index stream
|
||||
|
@ -3,6 +3,9 @@ import read from '../../common/read-messaging-message';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'messaging';
|
||||
public static shouldShare = false;
|
||||
|
||||
private otherpartyId: string;
|
||||
|
||||
@autobind
|
||||
|
@ -5,6 +5,9 @@ import Channel from '../channel';
|
||||
const ev = new Xev();
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'notesStats';
|
||||
public static shouldShare = true;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
ev.addListener('notesStats', this.onStats);
|
||||
|
@ -5,6 +5,9 @@ import Channel from '../channel';
|
||||
const ev = new Xev();
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'serverStats';
|
||||
public static shouldShare = true;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
ev.addListener('serverStats', this.onStats);
|
||||
|
@ -2,6 +2,9 @@ import autobind from 'autobind-decorator';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'userList';
|
||||
public static shouldShare = false;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
const listId = params.listId as string;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import * as websocket from 'websocket';
|
||||
import Xev from 'xev';
|
||||
import * as debug from 'debug';
|
||||
|
||||
import User, { IUser } from '../../../models/user';
|
||||
@ -11,6 +10,7 @@ import readNote from '../../../services/note/read';
|
||||
|
||||
import Channel from './channel';
|
||||
import channels from './channels';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
const log = debug('misskey');
|
||||
|
||||
@ -21,14 +21,14 @@ export default class Connection {
|
||||
public user?: IUser;
|
||||
public app: IApp;
|
||||
private wsConnection: websocket.connection;
|
||||
public subscriber: Xev;
|
||||
public subscriber: EventEmitter;
|
||||
private channels: Channel[] = [];
|
||||
private subscribingNotes: any = {};
|
||||
public sendMessageToWsOverride: any = null; // 後方互換性のため
|
||||
|
||||
constructor(
|
||||
wsConnection: websocket.connection,
|
||||
subscriber: Xev,
|
||||
subscriber: EventEmitter,
|
||||
user: IUser,
|
||||
app: IApp
|
||||
) {
|
||||
@ -146,9 +146,9 @@ export default class Connection {
|
||||
*/
|
||||
@autobind
|
||||
private onChannelConnectRequested(payload: any) {
|
||||
const { channel, id, params } = payload;
|
||||
const { channel, id, params, pong } = payload;
|
||||
log(`CH CONNECT: ${id} ${channel} by @${this.user.username}`);
|
||||
this.connectChannel(id, params, (channels as any)[channel]);
|
||||
this.connectChannel(id, params, channel, pong);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -177,13 +177,21 @@ export default class Connection {
|
||||
* チャンネルに接続
|
||||
*/
|
||||
@autobind
|
||||
public connectChannel(id: string, params: any, channelClass: { new(id: string, connection: Connection): Channel }) {
|
||||
const channel = new channelClass(id, this);
|
||||
this.channels.push(channel);
|
||||
channel.init(params);
|
||||
this.sendMessageToWs('connected', {
|
||||
id: id
|
||||
});
|
||||
public connectChannel(id: string, params: any, channel: string, pong = false) {
|
||||
// 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視
|
||||
if ((channels as any)[channel].shouldShare && this.channels.some(c => c.chName === channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ch: Channel = new (channels as any)[channel](id, this);
|
||||
this.channels.push(ch);
|
||||
ch.init(params);
|
||||
|
||||
if (pong) {
|
||||
this.sendMessageToWs('connected', {
|
||||
id: id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,11 +1,13 @@
|
||||
import * as http from 'http';
|
||||
import * as websocket from 'websocket';
|
||||
import * as redis from 'redis';
|
||||
import Xev from 'xev';
|
||||
|
||||
import MainStreamConnection from './stream';
|
||||
import { ParsedUrlQuery } from 'querystring';
|
||||
import authenticate from './authenticate';
|
||||
import channels from './stream/channels';
|
||||
import { EventEmitter } from 'events';
|
||||
import config from '../../config';
|
||||
|
||||
module.exports = (server: http.Server) => {
|
||||
// Init websocket server
|
||||
@ -16,11 +18,34 @@ module.exports = (server: http.Server) => {
|
||||
ws.on('request', async (request) => {
|
||||
const connection = request.accept();
|
||||
|
||||
const ev = new Xev();
|
||||
|
||||
const q = request.resourceURL.query as ParsedUrlQuery;
|
||||
const [user, app] = await authenticate(q.i as string);
|
||||
|
||||
let ev: EventEmitter;
|
||||
|
||||
if (config.redis) {
|
||||
// Connect to Redis
|
||||
const subscriber = redis.createClient(
|
||||
config.redis.port, config.redis.host);
|
||||
|
||||
subscriber.subscribe('misskey');
|
||||
|
||||
ev = new EventEmitter();
|
||||
|
||||
subscriber.on('message', async (_, data) => {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
ev.emit(obj.channel, obj.message);
|
||||
});
|
||||
|
||||
connection.once('close', () => {
|
||||
subscriber.unsubscribe();
|
||||
subscriber.quit();
|
||||
});
|
||||
} else {
|
||||
ev = new Xev();
|
||||
}
|
||||
|
||||
const main = new MainStreamConnection(connection, ev, user, app);
|
||||
|
||||
// 後方互換性のため
|
||||
@ -39,14 +64,14 @@ module.exports = (server: http.Server) => {
|
||||
}));
|
||||
};
|
||||
|
||||
main.connectChannel(Math.random().toString(), null,
|
||||
request.resourceURL.pathname === '/' ? channels.homeTimeline :
|
||||
request.resourceURL.pathname === '/local-timeline' ? channels.localTimeline :
|
||||
request.resourceURL.pathname === '/hybrid-timeline' ? channels.hybridTimeline :
|
||||
request.resourceURL.pathname === '/global-timeline' ? channels.globalTimeline : null);
|
||||
main.connectChannel(Math.random().toString().substr(2, 8), null,
|
||||
request.resourceURL.pathname === '/' ? 'homeTimeline' :
|
||||
request.resourceURL.pathname === '/local-timeline' ? 'localTimeline' :
|
||||
request.resourceURL.pathname === '/hybrid-timeline' ? 'hybridTimeline' :
|
||||
request.resourceURL.pathname === '/global-timeline' ? 'globalTimeline' : null);
|
||||
|
||||
if (request.resourceURL.pathname === '/') {
|
||||
main.connectChannel(Math.random().toString(), null, channels.main);
|
||||
main.connectChannel(Math.random().toString().substr(2, 8), null, 'main');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ module.exports = async (ctx: Koa.Context) => {
|
||||
try {
|
||||
const summary = config.summalyProxy ? await request.get({
|
||||
url: config.summalyProxy,
|
||||
proxy: config.proxy,
|
||||
qs: {
|
||||
url: ctx.query.url
|
||||
},
|
||||
|
@ -37,6 +37,7 @@ export default async (url: string, user: IUser, folderId: mongodb.ObjectID = nul
|
||||
const requestUrl = URL.parse(url).pathname.match(/[^\u0021-\u00ff]/) ? encodeURI(url) : url;
|
||||
request({
|
||||
url: requestUrl,
|
||||
proxy: config.proxy,
|
||||
headers: {
|
||||
'User-Agent': config.user_agent
|
||||
}
|
||||
|
@ -11,70 +11,75 @@ import { deliver } from '../../queue';
|
||||
import createFollowRequest from './requests/create';
|
||||
|
||||
export default async function(follower: IUser, followee: IUser) {
|
||||
if (followee.isLocked || isLocalUser(follower) && isRemoteUser(followee)) {
|
||||
// フォロー対象が鍵アカウントである or
|
||||
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
|
||||
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
|
||||
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
|
||||
if (followee.isLocked || (followee.carefulBot && follower.isBot) || (isLocalUser(follower) && isRemoteUser(followee))) {
|
||||
await createFollowRequest(follower, followee);
|
||||
} else {
|
||||
const following = await Following.insert({
|
||||
createdAt: new Date(),
|
||||
followerId: follower._id,
|
||||
followeeId: followee._id,
|
||||
return;
|
||||
}
|
||||
|
||||
// 非正規化
|
||||
_follower: {
|
||||
host: follower.host,
|
||||
inbox: isRemoteUser(follower) ? follower.inbox : undefined,
|
||||
sharedInbox: isRemoteUser(follower) ? follower.sharedInbox : undefined
|
||||
},
|
||||
_followee: {
|
||||
host: followee.host,
|
||||
inbox: isRemoteUser(followee) ? followee.inbox : undefined,
|
||||
sharedInbox: isRemoteUser(followee) ? followee.sharedInbox : undefined
|
||||
}
|
||||
});
|
||||
const following = await Following.insert({
|
||||
createdAt: new Date(),
|
||||
followerId: follower._id,
|
||||
followeeId: followee._id,
|
||||
|
||||
//#region Increment following count
|
||||
User.update({ _id: follower._id }, {
|
||||
$inc: {
|
||||
followingCount: 1
|
||||
}
|
||||
});
|
||||
|
||||
FollowingLog.insert({
|
||||
createdAt: following.createdAt,
|
||||
userId: follower._id,
|
||||
count: follower.followingCount + 1
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region Increment followers count
|
||||
User.update({ _id: followee._id }, {
|
||||
$inc: {
|
||||
followersCount: 1
|
||||
}
|
||||
});
|
||||
FollowedLog.insert({
|
||||
createdAt: following.createdAt,
|
||||
userId: followee._id,
|
||||
count: followee.followersCount + 1
|
||||
});
|
||||
//#endregion
|
||||
|
||||
// Publish follow event
|
||||
if (isLocalUser(follower)) {
|
||||
packUser(followee, follower).then(packed => publishMainStream(follower._id, 'follow', packed));
|
||||
// 非正規化
|
||||
_follower: {
|
||||
host: follower.host,
|
||||
inbox: isRemoteUser(follower) ? follower.inbox : undefined,
|
||||
sharedInbox: isRemoteUser(follower) ? follower.sharedInbox : undefined
|
||||
},
|
||||
_followee: {
|
||||
host: followee.host,
|
||||
inbox: isRemoteUser(followee) ? followee.inbox : undefined,
|
||||
sharedInbox: isRemoteUser(followee) ? followee.sharedInbox : undefined
|
||||
}
|
||||
});
|
||||
|
||||
// Publish followed event
|
||||
if (isLocalUser(followee)) {
|
||||
packUser(follower, followee).then(packed => publishMainStream(followee._id, 'followed', packed)),
|
||||
|
||||
// 通知を作成
|
||||
notify(followee._id, follower._id, 'follow');
|
||||
//#region Increment following count
|
||||
User.update({ _id: follower._id }, {
|
||||
$inc: {
|
||||
followingCount: 1
|
||||
}
|
||||
});
|
||||
|
||||
if (isRemoteUser(follower) && isLocalUser(followee)) {
|
||||
const content = pack(renderAccept(renderFollow(follower, followee)));
|
||||
deliver(followee, content, follower.inbox);
|
||||
FollowingLog.insert({
|
||||
createdAt: following.createdAt,
|
||||
userId: follower._id,
|
||||
count: follower.followingCount + 1
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region Increment followers count
|
||||
User.update({ _id: followee._id }, {
|
||||
$inc: {
|
||||
followersCount: 1
|
||||
}
|
||||
});
|
||||
FollowedLog.insert({
|
||||
createdAt: following.createdAt,
|
||||
userId: followee._id,
|
||||
count: followee.followersCount + 1
|
||||
});
|
||||
//#endregion
|
||||
|
||||
// Publish follow event
|
||||
if (isLocalUser(follower)) {
|
||||
packUser(followee, follower).then(packed => publishMainStream(follower._id, 'follow', packed));
|
||||
}
|
||||
|
||||
// Publish followed event
|
||||
if (isLocalUser(followee)) {
|
||||
packUser(follower, followee).then(packed => publishMainStream(followee._id, 'followed', packed)),
|
||||
|
||||
// 通知を作成
|
||||
notify(followee._id, follower._id, 'follow');
|
||||
}
|
||||
|
||||
if (isRemoteUser(follower) && isLocalUser(followee)) {
|
||||
const content = pack(renderAccept(renderFollow(follower, followee)));
|
||||
deliver(followee, content, follower.inbox);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import config from '../../config';
|
||||
import * as mongo from 'mongodb';
|
||||
import User, { isLocalUser, isRemoteUser, ILocalUser, IUser } from '../../models/user';
|
||||
import Note from '../../models/note';
|
||||
import Note, { packMany } from '../../models/note';
|
||||
import Following from '../../models/following';
|
||||
import renderAdd from '../../remote/activitypub/renderer/add';
|
||||
import renderRemove from '../../remote/activitypub/renderer/remove';
|
||||
@ -27,11 +27,11 @@ export async function addPinned(user: IUser, noteId: mongo.ObjectID) {
|
||||
let pinnedNoteIds = user.pinnedNoteIds || [];
|
||||
|
||||
//#region 現在ピン留め投稿している投稿が実際にデータベースに存在しているのかチェック
|
||||
// データベースの欠損などで存在していない場合があるので。
|
||||
// データベースの欠損などで存在していない(または破損している)場合があるので。
|
||||
// 存在していなかったらピン留め投稿から外す
|
||||
const pinnedNotes = (await Promise.all(pinnedNoteIds.map(id => Note.findOne({ _id: id })))).filter(x => x != null);
|
||||
const pinnedNotes = await packMany(pinnedNoteIds, null, { detail: true });
|
||||
|
||||
pinnedNoteIds = pinnedNoteIds.filter(id => pinnedNotes.some(n => n._id.equals(id)));
|
||||
pinnedNoteIds = pinnedNoteIds.filter(id => pinnedNotes.some(n => n.id.toString() === id.toHexString()));
|
||||
//#endregion
|
||||
|
||||
if (pinnedNoteIds.length >= 5) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as mongo from 'mongodb';
|
||||
import redis from './db/redis';
|
||||
import Xev from 'xev';
|
||||
import Meta, { IMeta } from './models/meta';
|
||||
|
||||
@ -9,7 +10,10 @@ class Publisher {
|
||||
private meta: IMeta;
|
||||
|
||||
constructor() {
|
||||
this.ev = new Xev();
|
||||
// Redisがインストールされてないときはプロセス間通信を使う
|
||||
if (redis == null) {
|
||||
this.ev = new Xev();
|
||||
}
|
||||
|
||||
setInterval(async () => {
|
||||
this.meta = await Meta.findOne({});
|
||||
@ -28,7 +32,14 @@ class Publisher {
|
||||
{ type: type, body: null } :
|
||||
{ type: type, body: value };
|
||||
|
||||
this.ev.emit(channel, message);
|
||||
if (this.ev) {
|
||||
this.ev.emit(channel, message);
|
||||
} else {
|
||||
redis.publish('misskey', JSON.stringify({
|
||||
channel: channel,
|
||||
message: message
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public publishMainStream = (userId: ID, type: string, value?: any): void => {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user