mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-08-02 05:16:01 +00:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8620187ec | |||
| 520849d070 | |||
| b6a028a8ed | |||
| af5839bb59 | |||
| a53e0d9f73 | |||
| 49921f2dcf | |||
| 6b947c2139 | |||
| 98acf919f1 | |||
| c9c2853150 | |||
| 2bc708f8e6 | |||
| 874b8fc3c2 | |||
| 7d6aac3431 | |||
| e2fc7decad | |||
| 21bed71f5e | |||
| 3b974428fc | |||
| 580191fb17 | |||
| be0cb88b6c | |||
| 95c4e4497e | |||
| 2ec445f83e | |||
| 51b915428e | |||
| 1395cf89ce | |||
| 2a8f984db7 | |||
| decf2d396f | |||
| f7964da899 | |||
| c8607ff7b6 | |||
| e9f8897fe2 | |||
| e0b107a3a0 | |||
| 1d3e6a7197 | |||
| 1c93fcb1c4 | |||
| e3389e7899 | |||
| 454632d785 | |||
| c9bca7dc85 | |||
| 710ba526fa | |||
| aa47b6732d | |||
| 20f83420ca | |||
| d09a68ef11 | |||
| b545be5799 | |||
| 4fc377584f | |||
| a5f09c90dd | |||
| d059d7f972 | |||
| c03e2dfbc0 | |||
| 45c5e7b967 | |||
| c81a94ff75 | |||
| acc6f54557 | |||
| 8025b121af | |||
| 78ec06bda3 | |||
| 6ef83d9c59 | |||
| fca4ceef21 | |||
| 00f979f0e6 | |||
| 556677be7a | |||
| 624fd093f2 | |||
| 2ee438dece | |||
| 534de24406 | |||
| e88ce1746d | |||
| b8aad35009 | |||
| 47bd485a39 | |||
| ad869d7469 | |||
| d15cce5337 | |||
| 37daff6d61 | |||
| 5417e40f59 | |||
| 0fed33bfdb | |||
| 5dddc75d09 | |||
| 081578c604 | |||
| 6c47bf5b76 | |||
| 9e85291cd3 | |||
| 7f77517fc8 | |||
| b2f288dcac | |||
| 52b59e9d7b | |||
| 80c74b1fa7 | |||
| 91811ea500 | |||
| 57150fd910 | |||
| cddbbdf5d0 | |||
| 423dc2349b | |||
| 0556a2a2da | |||
| 65d943e42a | |||
| 3bcb344ecb | |||
| 82d721d60b |
@@ -108,13 +108,5 @@ autoAdmin: true
|
|||||||
# port: 9200
|
# port: 9200
|
||||||
# pass: null
|
# pass: null
|
||||||
|
|
||||||
# ServiceWorker
|
|
||||||
#sw:
|
|
||||||
# # Public key of VAPID
|
|
||||||
# public_key: example-sw-public-key
|
|
||||||
#
|
|
||||||
# # Private key of VAPID
|
|
||||||
# private_key: example-sw-private-key
|
|
||||||
|
|
||||||
# Clustering
|
# Clustering
|
||||||
#clusterLimit: 1
|
#clusterLimit: 1
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Summary
|
||||||
|
|
||||||
|
<!--
|
||||||
|
-
|
||||||
|
- * Please describe your changes here *
|
||||||
|
-
|
||||||
|
- If you are going to resolve some issue, please add this context.
|
||||||
|
- Resolve #ISSUE_NUMBER
|
||||||
|
-
|
||||||
|
- If you are going to fix some bug issue, please add this context.
|
||||||
|
- Fix #ISSUE_NUMBER
|
||||||
|
-
|
||||||
|
-->
|
||||||
@@ -1,6 +1,52 @@
|
|||||||
ChangeLog
|
ChangeLog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
10.66.2
|
||||||
|
-------
|
||||||
|
* i18nの修正
|
||||||
|
* ドライブのファイル一覧取得APIでファイルサイズによるソートが機能していなかった問題を修正
|
||||||
|
* リモートユーザーの更新時に、各ピン留め投稿の取得失敗は無視するように
|
||||||
|
* リモートMisskeyユーザーの情報が登録/更新出来なくなっていたのを修正
|
||||||
|
* メンションのリンク先URLに余計な@がプリフィクスされていたのを修正
|
||||||
|
* ダイレクトでリプライする際、リプライ先のユーザーは自動的に公開先として追加するように
|
||||||
|
* ダイレクトでメンションでもユーザーを指定できるように
|
||||||
|
|
||||||
|
10.66.1
|
||||||
|
-------
|
||||||
|
* ActivityPubのsharedInboxに関して修正
|
||||||
|
* MFMでのカッコの判定を改善
|
||||||
|
* バグ修正
|
||||||
|
|
||||||
|
10.66.0
|
||||||
|
-------
|
||||||
|
* ユーザーごとのRSSフィードを提供するように
|
||||||
|
* リストのユーザーがすべて表示できない問題を修正
|
||||||
|
* デザインの調整
|
||||||
|
* パフォーマンスの改善
|
||||||
|
|
||||||
|
10.65.0
|
||||||
|
-------
|
||||||
|
* 検索で投稿やユーザーのURLを入力した際にそれをフェッチして表示するように
|
||||||
|
* リストのリネームと削除をできるように
|
||||||
|
* リストからユーザーを削除できるように
|
||||||
|
* リモートの絵文字を更新するように
|
||||||
|
* ActivityPubのための絵文字エンドポイントを実装
|
||||||
|
* 管理者がドライブのファイルのNSFWを設定できるように
|
||||||
|
* ServiceWorkerの設定を管理者ページで行えるように
|
||||||
|
* メンションの判定を改善
|
||||||
|
* リモートの投稿を引用した際にオリジナルのURLを挿入するように
|
||||||
|
* クライアントのパフォーマンス改善
|
||||||
|
* CWの内容がタブタイトルに表示されるのを修正
|
||||||
|
* アカウントを作成したときにログイン状態にならない問題を修正
|
||||||
|
* 時計の針にテーマカラーが適用されていなかったのを修正
|
||||||
|
* 一部の日時の表示が日本語で表示されていたのを修正
|
||||||
|
* プロフィールの写真欄に画像以外のファイルが含まれる問題を修正
|
||||||
|
* メンションが含まれる投稿に返信する際、フォームに予めそれらのメンションがセットされた状態にならない問題を修正
|
||||||
|
* デッキのTLにUIの動きを減らすオプションが適用されていなかったのを修正
|
||||||
|
* ログイン画面のタイムラインに隠した投稿が表示される問題を修正
|
||||||
|
* サジェストが複数開いてしまう問題を修正
|
||||||
|
* APから来たタグに登録時の長さ制限が適用されていなかったのを修正
|
||||||
|
|
||||||
10.64.2
|
10.64.2
|
||||||
-------
|
-------
|
||||||
* UIの動きを減らすオプションが一部のアニメーションに適用されなかったのを修正
|
* UIの動きを減らすオプションが一部のアニメーションに適用されなかったのを修正
|
||||||
|
|||||||
@@ -25,3 +25,16 @@ Misskey uses [vue-i18n](https://github.com/kazupon/vue-i18n).
|
|||||||
## Continuous integration
|
## Continuous integration
|
||||||
Misskey uses CircleCI for automated test.
|
Misskey uses CircleCI for automated test.
|
||||||
Configuration files are located in `/.circleci`.
|
Configuration files are located in `/.circleci`.
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
### AP
|
||||||
|
Stands for _**A**ctivity**P**ub_.
|
||||||
|
|
||||||
|
### MFM
|
||||||
|
Stands for _**M**isskey **F**lavored **M**arkdown_.
|
||||||
|
|
||||||
|
### Mk
|
||||||
|
Stands for _**M**iss**k**ey_.
|
||||||
|
|
||||||
|
### SW
|
||||||
|
Stands for _**S**ervice**W**orker_.
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Sample nginx configuration for Misskey
|
||||||
|
#
|
||||||
|
# 1. Replace example.tld to your domain
|
||||||
|
# 2. Copy to /etc/nginx/sites-available/ and then symlink from /etc/nginx/sites-ebabled/
|
||||||
|
# or copy to /etc/nginx/conf.d/
|
||||||
|
|
||||||
|
# For WebSocket
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name example.tld;
|
||||||
|
|
||||||
|
# For SSL domain validation
|
||||||
|
root /var/www/html;
|
||||||
|
location /.well-known/acme-challenge/ { allow all; }
|
||||||
|
location /.well-known/pki-validation/ { allow all; }
|
||||||
|
location / { return 301 https://$server_name$request_uri; }
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 http2;
|
||||||
|
listen [::]:443 http2;
|
||||||
|
server_name example.tld;
|
||||||
|
ssl on;
|
||||||
|
ssl_session_cache shared:ssl_session_cache:10m;
|
||||||
|
|
||||||
|
# To use Let's Encrypt certificate
|
||||||
|
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
|
||||||
|
|
||||||
|
# To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
|
||||||
|
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
|
||||||
|
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
|
||||||
|
|
||||||
|
# SSL protocol settings
|
||||||
|
ssl_protocols TLSv1 TLSv1.2;
|
||||||
|
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES128-SHA;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
# Change to your upload limit
|
||||||
|
client_max_body_size 80m;
|
||||||
|
|
||||||
|
# Proxy to Node
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_redirect off;
|
||||||
|
|
||||||
|
# For WebSocket
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
|
||||||
|
# Cache settings
|
||||||
|
proxy_cache cache1;
|
||||||
|
proxy_cache_lock on;
|
||||||
|
proxy_cache_use_stale updating;
|
||||||
|
add_header X-Cache $upstream_cache_status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,16 +47,6 @@ As root:
|
|||||||
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest)
|
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest)
|
||||||
5. `npm install` Install misskey dependencies.
|
5. `npm install` Install misskey dependencies.
|
||||||
|
|
||||||
*(optional)* Generate VAPID keys
|
|
||||||
----------------------------------------------------------------
|
|
||||||
If you want to enable ServiceWorker, you need to generate VAPID keys:
|
|
||||||
Unless you have set your global node_modules location elsewhere, you need to run this as root.
|
|
||||||
|
|
||||||
``` shell
|
|
||||||
npm install web-push -g
|
|
||||||
web-push generate-vapid-keys
|
|
||||||
```
|
|
||||||
|
|
||||||
*5.* Configure Misskey
|
*5.* Configure Misskey
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`.
|
1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`.
|
||||||
|
|||||||
@@ -47,16 +47,6 @@ En mode root :
|
|||||||
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Télécharge la [version la plus récente](https://github.com/syuilo/misskey/releases/latest)
|
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Télécharge la [version la plus récente](https://github.com/syuilo/misskey/releases/latest)
|
||||||
5. `npm install` Installez les dépendances de misskey.
|
5. `npm install` Installez les dépendances de misskey.
|
||||||
|
|
||||||
*(optionnel)* Génération des clés VAPID
|
|
||||||
----------------------------------------------------------------
|
|
||||||
Si vous désirez activer ServiceWorker, vous devez générer les clés VAPID :
|
|
||||||
Unless you have set your global node_modules location elsewhere, vous devez lancer ceci en mode root.
|
|
||||||
|
|
||||||
``` shell
|
|
||||||
npm install web-push -g
|
|
||||||
web-push generate-vapid-keys
|
|
||||||
```
|
|
||||||
|
|
||||||
*5.* Création du fichier de configuration
|
*5.* Création du fichier de configuration
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
1. `cp .config/example.yml .config/default.yml` Copiez le fichier `.config/example.yml` et renommez-le `default.yml`.
|
1. `cp .config/example.yml .config/default.yml` Copiez le fichier `.config/example.yml` et renommez-le `default.yml`.
|
||||||
|
|||||||
@@ -53,15 +53,6 @@ adduser --disabled-password --disabled-login misskey
|
|||||||
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
|
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
|
||||||
5. `npm install` Misskeyの依存パッケージをインストール
|
5. `npm install` Misskeyの依存パッケージをインストール
|
||||||
|
|
||||||
*(オプション)* VAPIDキーペアの生成
|
|
||||||
----------------------------------------------------------------
|
|
||||||
ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります:
|
|
||||||
|
|
||||||
``` shell
|
|
||||||
npm install web-push -g
|
|
||||||
web-push generate-vapid-keys
|
|
||||||
```
|
|
||||||
|
|
||||||
*5.* 設定ファイルを作成する
|
*5.* 設定ファイルを作成する
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする。
|
1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする。
|
||||||
|
|||||||
+1
-1
@@ -805,6 +805,7 @@ desktop/views/components/settings.vue:
|
|||||||
update-settings: "Advanced settings"
|
update-settings: "Advanced settings"
|
||||||
prevent-update: "Postpone updates (not recommended)"
|
prevent-update: "Postpone updates (not recommended)"
|
||||||
prevent-update-desc: "Even if you turn this setting on, updates may apply. This setting is enabled only for this device."
|
prevent-update-desc: "Even if you turn this setting on, updates may apply. This setting is enabled only for this device."
|
||||||
|
mark-as-read-all-unread-notes: "Mark all posts as read"
|
||||||
no-updates: "No updates available"
|
no-updates: "No updates available"
|
||||||
no-updates-desc: "Your Misskey is up to date."
|
no-updates-desc: "Your Misskey is up to date."
|
||||||
update-available: "A new version is available"
|
update-available: "A new version is available"
|
||||||
@@ -1430,7 +1431,6 @@ mobile/views/pages/settings.vue:
|
|||||||
signout: "Sign out"
|
signout: "Sign out"
|
||||||
sound: "Sounds"
|
sound: "Sounds"
|
||||||
enable-sounds: "Enable sounds"
|
enable-sounds: "Enable sounds"
|
||||||
mark-as-read-all-unread-notes: "Mark all posts as read"
|
|
||||||
password: "Password"
|
password: "Password"
|
||||||
mobile/views/pages/user.vue:
|
mobile/views/pages/user.vue:
|
||||||
follows-you: "Follows you"
|
follows-you: "Follows you"
|
||||||
|
|||||||
+17
-1
@@ -519,6 +519,14 @@ common/views/components/profile-editor.vue:
|
|||||||
email-verified: "メールアドレスが確認されました"
|
email-verified: "メールアドレスが確認されました"
|
||||||
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
|
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
|
||||||
|
|
||||||
|
common/views/components/user-list-editor.vue:
|
||||||
|
users: "ユーザー"
|
||||||
|
rename: "リスト名を変更"
|
||||||
|
delete: "リストを削除"
|
||||||
|
remove-user: "このリストから削除"
|
||||||
|
delete-are-you-sure: "リスト「$1」を削除しますか?"
|
||||||
|
deleted: "削除しました"
|
||||||
|
|
||||||
common/views/widgets/broadcast.vue:
|
common/views/widgets/broadcast.vue:
|
||||||
fetching: "確認中"
|
fetching: "確認中"
|
||||||
no-broadcasts: "お知らせはありません"
|
no-broadcasts: "お知らせはありません"
|
||||||
@@ -830,6 +838,7 @@ desktop/views/components/settings.vue:
|
|||||||
2fa: "二段階認証"
|
2fa: "二段階認証"
|
||||||
other: "その他"
|
other: "その他"
|
||||||
license: "ライセンス"
|
license: "ライセンス"
|
||||||
|
mark-as-read-all-unread-notes: "すべての投稿を既読にする"
|
||||||
theme: "テーマ"
|
theme: "テーマ"
|
||||||
|
|
||||||
behaviour: "動作"
|
behaviour: "動作"
|
||||||
@@ -1156,6 +1165,12 @@ admin/views/instance.vue:
|
|||||||
smtp-port: "SMTPポート"
|
smtp-port: "SMTPポート"
|
||||||
smtp-user: "SMTPユーザー"
|
smtp-user: "SMTPユーザー"
|
||||||
smtp-pass: "SMTPパスワード"
|
smtp-pass: "SMTPパスワード"
|
||||||
|
serviceworker-config: "ServiceWorker"
|
||||||
|
enable-serviceworker: "ServiceWorkerを有効にする"
|
||||||
|
serviceworker-info: "プッシュ通知を行うには有効する必要があります。"
|
||||||
|
vapid-publickey: "VAPID公開鍵"
|
||||||
|
vapid-privatekey: "VAPID秘密鍵"
|
||||||
|
vapid-info: "ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります。シェルで次のようにします:"
|
||||||
|
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
@@ -1197,6 +1212,8 @@ admin/views/drive.vue:
|
|||||||
remote: "リモート"
|
remote: "リモート"
|
||||||
delete: "削除"
|
delete: "削除"
|
||||||
deleted: "削除しました"
|
deleted: "削除しました"
|
||||||
|
mark-as-sensitive: "閲覧注意に設定"
|
||||||
|
unmark-as-sensitive: "閲覧注意を解除"
|
||||||
|
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
operation: "操作"
|
operation: "操作"
|
||||||
@@ -1625,7 +1642,6 @@ mobile/views/pages/settings.vue:
|
|||||||
signout: "サインアウト"
|
signout: "サインアウト"
|
||||||
sound: "サウンド"
|
sound: "サウンド"
|
||||||
enable-sounds: "サウンドを有効にする"
|
enable-sounds: "サウンドを有効にする"
|
||||||
mark-as-read-all-unread-notes: "すべての投稿を既読にする"
|
|
||||||
password: "パスワード"
|
password: "パスワード"
|
||||||
|
|
||||||
mobile/views/pages/user.vue:
|
mobile/views/pages/user.vue:
|
||||||
|
|||||||
+3
-2
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "10.64.2",
|
"version": "10.66.2",
|
||||||
"clientVersion": "2.0.12796",
|
"clientVersion": "2.0.12873",
|
||||||
"codename": "nighthike",
|
"codename": "nighthike",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -114,6 +114,7 @@
|
|||||||
"eslint": "5.8.0",
|
"eslint": "5.8.0",
|
||||||
"eslint-plugin-vue": "4.7.1",
|
"eslint-plugin-vue": "4.7.1",
|
||||||
"eventemitter3": "3.1.0",
|
"eventemitter3": "3.1.0",
|
||||||
|
"feed": "2.0.2",
|
||||||
"file-loader": "2.0.0",
|
"file-loader": "2.0.0",
|
||||||
"file-type": "10.6.0",
|
"file-type": "10.6.0",
|
||||||
"fuckadblock": "3.2.1",
|
"fuckadblock": "3.2.1",
|
||||||
|
|||||||
@@ -39,7 +39,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="file._open">
|
<div v-show="file._open">
|
||||||
<ui-button @click="del(file)"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
|
<ui-horizon-group>
|
||||||
|
<ui-button @click="toggleSensitive(file)" v-if="file.isSensitive"><fa :icon="faEye"/> {{ $t('unmark-as-sensitive') }}</ui-button>
|
||||||
|
<ui-button @click="toggleSensitive(file)" v-else><fa :icon="faEyeSlash"/> {{ $t('mark-as-sensitive') }}</ui-button>
|
||||||
|
<ui-button @click="del(file)"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
|
||||||
|
</ui-horizon-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</sequential-entrance>
|
</sequential-entrance>
|
||||||
@@ -53,7 +57,7 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../i18n';
|
import i18n from '../../i18n';
|
||||||
import { faCloud } from '@fortawesome/free-solid-svg-icons';
|
import { faCloud } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
|
import { faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('admin/views/drive.vue'),
|
i18n: i18n('admin/views/drive.vue'),
|
||||||
@@ -66,7 +70,7 @@ export default Vue.extend({
|
|||||||
offset: 0,
|
offset: 0,
|
||||||
files: [],
|
files: [],
|
||||||
existMore: false,
|
existMore: false,
|
||||||
faCloud, faTrashAlt
|
faCloud, faTrashAlt, faEye, faEyeSlash
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -132,7 +136,16 @@ export default Vue.extend({
|
|||||||
text: e.toString()
|
text: e.toString()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
|
toggleSensitive(file: any) {
|
||||||
|
this.$root.api('drive/files/update', {
|
||||||
|
fileId: file.id,
|
||||||
|
isSensitive: !file.isSensitive
|
||||||
|
});
|
||||||
|
|
||||||
|
file.isSensitive = !file.isSensitive;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -57,6 +57,15 @@
|
|||||||
</ui-horizon-group>
|
</ui-horizon-group>
|
||||||
<ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<span slot="desc">{{ $t('smtp-secure-info') }}</span></ui-switch>
|
<ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<span slot="desc">{{ $t('smtp-secure-info') }}</span></ui-switch>
|
||||||
</section>
|
</section>
|
||||||
|
<section>
|
||||||
|
<header><fa :icon="faBolt"/> {{ $t('serviceworker-config') }}</header>
|
||||||
|
<ui-switch v-model="enableServiceWorker">{{ $t('enable-serviceworker') }}<span slot="desc">{{ $t('serviceworker-info') }}</span></ui-switch>
|
||||||
|
<ui-info>{{ $t('vapid-info') }}<br><code>npm i web-push -g<br>web-push generate-vapid-keys</code></ui-info>
|
||||||
|
<ui-horizon-group inputs class="fit-bottom">
|
||||||
|
<ui-input v-model="swPublicKey" :disabled="!enableServiceWorker"><i slot="icon"><fa icon="key"/></i>{{ $t('vapid-publickey') }}</ui-input>
|
||||||
|
<ui-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><i slot="icon"><fa icon="key"/></i>{{ $t('vapid-privatekey') }}</ui-input>
|
||||||
|
</ui-horizon-group>
|
||||||
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<header>summaly Proxy</header>
|
<header>summaly Proxy</header>
|
||||||
<ui-input v-model="summalyProxy">URL</ui-input>
|
<ui-input v-model="summalyProxy">URL</ui-input>
|
||||||
@@ -126,7 +135,7 @@ import Vue from 'vue';
|
|||||||
import i18n from '../../i18n';
|
import i18n from '../../i18n';
|
||||||
import { url, host } from '../../config';
|
import { url, host } from '../../config';
|
||||||
import { toUnicode } from 'punycode';
|
import { toUnicode } from 'punycode';
|
||||||
import { faHeadset, faShieldAlt, faGhost, faUserPlus } from '@fortawesome/free-solid-svg-icons';
|
import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faEnvelope as farEnvelope } from '@fortawesome/free-regular-svg-icons';
|
import { faEnvelope as farEnvelope } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
@@ -174,7 +183,10 @@ export default Vue.extend({
|
|||||||
smtpPort: null,
|
smtpPort: null,
|
||||||
smtpUser: null,
|
smtpUser: null,
|
||||||
smtpPass: null,
|
smtpPass: null,
|
||||||
faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope
|
enableServiceWorker: false,
|
||||||
|
swPublicKey: null,
|
||||||
|
swPrivateKey: null,
|
||||||
|
faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -217,6 +229,9 @@ export default Vue.extend({
|
|||||||
this.smtpPort = meta.smtpPort;
|
this.smtpPort = meta.smtpPort;
|
||||||
this.smtpUser = meta.smtpUser;
|
this.smtpUser = meta.smtpUser;
|
||||||
this.smtpPass = meta.smtpPass;
|
this.smtpPass = meta.smtpPass;
|
||||||
|
this.enableServiceWorker = meta.enableServiceWorker;
|
||||||
|
this.swPublicKey = meta.swPublickey;
|
||||||
|
this.swPrivateKey = meta.swPrivateKey;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -270,7 +285,10 @@ export default Vue.extend({
|
|||||||
smtpHost: this.smtpHost,
|
smtpHost: this.smtpHost,
|
||||||
smtpPort: parseInt(this.smtpPort, 10),
|
smtpPort: parseInt(this.smtpPort, 10),
|
||||||
smtpUser: this.smtpUser,
|
smtpUser: this.smtpUser,
|
||||||
smtpPass: this.smtpPass
|
smtpPass: this.smtpPass,
|
||||||
|
enableServiceWorker: this.enableServiceWorker,
|
||||||
|
swPublicKey: this.swPublicKey,
|
||||||
|
swPrivateKey: this.swPrivateKey
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.$root.dialog({
|
this.$root.dialog({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function(type, data): Notification {
|
|||||||
|
|
||||||
case 'unreadMessagingMessage':
|
case 'unreadMessagingMessage':
|
||||||
return {
|
return {
|
||||||
title: '%i18n:common.notification.message-from%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.message-from%'.split("{}")[1] ,
|
title: '%i18n:common.notification.message-from%'.split('{}')[0] + `${getUserName(data.user)}` + '%i18n:common.notification.message-from%'.split('{}')[1] ,
|
||||||
body: data.text, // TODO: getMessagingMessageSummary(data),
|
body: data.text, // TODO: getMessagingMessageSummary(data),
|
||||||
icon: data.user.avatarUrl
|
icon: data.user.avatarUrl
|
||||||
};
|
};
|
||||||
@@ -30,7 +30,7 @@ export default function(type, data): Notification {
|
|||||||
case 'reversiInvited':
|
case 'reversiInvited':
|
||||||
return {
|
return {
|
||||||
title: '%i18n:common.notification.reversi-invited%',
|
title: '%i18n:common.notification.reversi-invited%',
|
||||||
body: '%i18n:common.notification.reversi-invited-by%'.split("{}")[0] + `${getUserName(data.parent)}` + '%i18n:common.notification.reversi-invited-by%'.split("{}")[1],
|
body: '%i18n:common.notification.reversi-invited-by%'.split('{}')[0] + `${getUserName(data.parent)}` + '%i18n:common.notification.reversi-invited-by%'.split('{}')[1],
|
||||||
icon: data.parent.avatarUrl
|
icon: data.parent.avatarUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,21 +38,21 @@ export default function(type, data): Notification {
|
|||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'mention':
|
case 'mention':
|
||||||
return {
|
return {
|
||||||
title: '%i18n:common.notification.notified-by%'.split("{}")[0] + `${getUserName(data.user)}:` + '%i18n:common.notification.notified-by%'.split("{}")[1],
|
title: '%i18n:common.notification.notified-by%'.split('{}')[0] + `${getUserName(data.user)}:` + '%i18n:common.notification.notified-by%'.split('{}')[1],
|
||||||
body: getNoteSummary(data),
|
body: getNoteSummary(data),
|
||||||
icon: data.user.avatarUrl
|
icon: data.user.avatarUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'reply':
|
case 'reply':
|
||||||
return {
|
return {
|
||||||
title: '%i18n:common.notification.reply-from%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.reply-from%'.split("{}")[1],
|
title: '%i18n:common.notification.reply-from%'.split('{}')[0] + `${getUserName(data.user)}` + '%i18n:common.notification.reply-from%'.split('{}')[1],
|
||||||
body: getNoteSummary(data),
|
body: getNoteSummary(data),
|
||||||
icon: data.user.avatarUrl
|
icon: data.user.avatarUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'quote':
|
case 'quote':
|
||||||
return {
|
return {
|
||||||
title: '%i18n:common.notification.quoted-by%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.quoted-by%'.split("{}")[1],
|
title: '%i18n:common.notification.quoted-by%'.split('{}')[0] + `${getUserName(data.user)}` + '%i18n:common.notification.quoted-by%'.split('{}')[1],
|
||||||
body: getNoteSummary(data),
|
body: getNoteSummary(data),
|
||||||
icon: data.user.avatarUrl
|
icon: data.user.avatarUrl
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default function(sec) {
|
|||||||
const t
|
const t
|
||||||
= tod < 60 ? `${Math.floor(tod)} sec`
|
= tod < 60 ? `${Math.floor(tod)} sec`
|
||||||
: tod < 3600 ? `${Math.floor(tod / 60)} min`
|
: tod < 3600 ? `${Math.floor(tod / 60)} min`
|
||||||
: `${Math.floor(tod / 60 / 60)}:${Math.floor((tod / 60) % 60).toString().padStart(2, "0")}`;
|
: `${Math.floor(tod / 60 / 60)}:${Math.floor((tod / 60) % 60).toString().padStart(2, '0')}`;
|
||||||
|
|
||||||
let str = '';
|
let str = '';
|
||||||
if (d) str += `${d}, `;
|
if (d) str += `${d}, `;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
export default (data: ArrayBuffer) => {
|
export default (data: ArrayBuffer) => {
|
||||||
//const buf = new Buffer(data);
|
//const buf = new Buffer(data);
|
||||||
//const hash = crypto.createHash("md5");
|
//const hash = crypto.createHash('md5');
|
||||||
//hash.update(buf);
|
//hash.update(buf);
|
||||||
//return hash.digest("hex");
|
//return hash.digest('hex');
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ export default (opts: Opts = {}) => ({
|
|||||||
const ast = parse(this.appearNote.text);
|
const ast = parse(this.appearNote.text);
|
||||||
// TODO: 再帰的にURL要素がないか調べる
|
// TODO: 再帰的にURL要素がないか調べる
|
||||||
return unique(ast
|
return unique(ast
|
||||||
.filter(t => ((t.name == 'url' || t.name == 'link') && t.props.url && !t.props.silent))
|
.filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent))
|
||||||
.map(t => t.props.url));
|
.map(t => t.node.props.url));
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export default Vue.extend({
|
|||||||
return this.dark ? '#fff' : '#777';
|
return this.dark ? '#fff' : '#777';
|
||||||
},
|
},
|
||||||
hHandColor(): string {
|
hHandColor(): string {
|
||||||
return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--themeColor')).toHexString();
|
return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--primary')).toHexString();
|
||||||
},
|
},
|
||||||
|
|
||||||
ms(): number {
|
ms(): number {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default Vue.extend({
|
|||||||
iconAndText(): any[] {
|
iconAndText(): any[] {
|
||||||
return (
|
return (
|
||||||
(this.hasPendingFollowRequestFromYou && this.user.isLocked) ? ['hourglass-half', this.$t('request-pending')] :
|
(this.hasPendingFollowRequestFromYou && this.user.isLocked) ? ['hourglass-half', this.$t('request-pending')] :
|
||||||
(this.hasPendingFollowRequestFromYou && !this.user.isLocked) ? ['hourglass-start', this.$t('follow-processing')] :
|
(this.hasPendingFollowRequestFromYou && !this.user.isLocked) ? ['spinner', this.$t('follow-processing')] :
|
||||||
(this.isFollowing) ? ['minus', this.$t('following')] :
|
(this.isFollowing) ? ['minus', this.$t('following')] :
|
||||||
(!this.isFollowing && this.user.isLocked) ? ['plus', this.$t('follow-request')] :
|
(!this.isFollowing && this.user.isLocked) ? ['plus', this.$t('follow-request')] :
|
||||||
(!this.isFollowing && !this.user.isLocked) ? ['plus', this.$t('follow')] :
|
(!this.isFollowing && !this.user.isLocked) ? ['plus', this.$t('follow')] :
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link class="ldlomzub" :to="`/@${ canonical }`" v-user-preview="canonical">
|
<router-link class="ldlomzub" :to="`/${ canonical }`" v-user-preview="canonical">
|
||||||
<span class="me" v-if="isMe">{{ $t('@.you') }}</span>
|
<span class="me" v-if="isMe">{{ $t('@.you') }}</span>
|
||||||
<span class="main">
|
<span class="main">
|
||||||
<span class="username">@{{ username }}</span>
|
<span class="username">@{{ username }}</span>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (items[0].kind == 'file') {
|
if (items[0].kind == 'file') {
|
||||||
alert('%i18n:only-one-file-attached%');
|
alert(this.$t('only-one-file-attached'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -107,7 +107,7 @@ export default Vue.extend({
|
|||||||
return;
|
return;
|
||||||
} else if (e.dataTransfer.files.length > 1) {
|
} else if (e.dataTransfer.files.length > 1) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('%i18n:only-one-file-attached%');
|
alert(this.$t('only-one-file-attached'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ export default Vue.extend({
|
|||||||
if (this.message.text) {
|
if (this.message.text) {
|
||||||
const ast = parse(this.message.text);
|
const ast = parse(this.message.text);
|
||||||
return unique(ast
|
return unique(ast
|
||||||
.filter(t => ((t.name == 'url' || t.name == 'link') && t.props.url && !t.silent))
|
.filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent))
|
||||||
.map(t => t.props.url));
|
.map(t => t.node.props.url));
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Vue, { VNode } from 'vue';
|
import Vue, { VNode } from 'vue';
|
||||||
import { length } from 'stringz';
|
import { length } from 'stringz';
|
||||||
import { Node } from '../../../../../mfm/parser';
|
import { MfmForest } from '../../../../../mfm/parser';
|
||||||
import parse from '../../../../../mfm/parse';
|
import parse from '../../../../../mfm/parse';
|
||||||
import MkUrl from './url.vue';
|
import MkUrl from './url.vue';
|
||||||
import MkMention from './mention.vue';
|
import MkMention from './mention.vue';
|
||||||
@@ -9,16 +9,11 @@ import MkFormula from './formula.vue';
|
|||||||
import MkGoogle from './google.vue';
|
import MkGoogle from './google.vue';
|
||||||
import syntaxHighlight from '../../../../../mfm/syntax-highlight';
|
import syntaxHighlight from '../../../../../mfm/syntax-highlight';
|
||||||
import { host } from '../../../config';
|
import { host } from '../../../config';
|
||||||
|
import { preorderF, countNodesF } from '../../../../../prelude/tree';
|
||||||
|
|
||||||
function getTextCount(tokens: Node[]): number {
|
function sumTextsLength(ts: MfmForest): number {
|
||||||
const rootCount = sum(tokens.filter(x => x.name === 'text').map(x => length(x.props.text)));
|
const textNodes = preorderF(ts).filter(n => n.type === 'text');
|
||||||
const childrenCount = sum(tokens.filter(x => x.children).map(x => getTextCount(x.children)));
|
return sum(textNodes.map(x => length(x.props.text)));
|
||||||
return rootCount + childrenCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getChildrenCount(tokens: Node[]): number {
|
|
||||||
const countTree = tokens.filter(x => x.children).map(x => getChildrenCount(x.children));
|
|
||||||
return countTree.length + sum(countTree);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Vue.component('misskey-flavored-markdown', {
|
export default Vue.component('misskey-flavored-markdown', {
|
||||||
@@ -27,10 +22,6 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
ast: {
|
|
||||||
type: [],
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
shouldBreak: {
|
shouldBreak: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
@@ -55,17 +46,15 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
render(createElement) {
|
render(createElement) {
|
||||||
if (this.text == null || this.text == '') return;
|
if (this.text == null || this.text == '') return;
|
||||||
|
|
||||||
const ast = this.ast == null ?
|
const ast = parse(this.text, this.plainText);
|
||||||
parse(this.text, this.plainText) : // Parse text to ast
|
|
||||||
this.ast as Node[];
|
|
||||||
|
|
||||||
let bigCount = 0;
|
let bigCount = 0;
|
||||||
let motionCount = 0;
|
let motionCount = 0;
|
||||||
|
|
||||||
const genEl = (ast: Node[]) => concat(ast.map((token): VNode[] => {
|
const genEl = (ast: MfmForest) => concat(ast.map((token): VNode[] => {
|
||||||
switch (token.name) {
|
switch (token.node.type) {
|
||||||
case 'text': {
|
case 'text': {
|
||||||
const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
|
const text = token.node.props.text.replace(/(\r\n|\n|\r)/g, '\n');
|
||||||
|
|
||||||
if (this.shouldBreak) {
|
if (this.shouldBreak) {
|
||||||
const x = text.split('\n')
|
const x = text.split('\n')
|
||||||
@@ -95,7 +84,7 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
|
|
||||||
case 'big': {
|
case 'big': {
|
||||||
bigCount++;
|
bigCount++;
|
||||||
const isLong = getTextCount(token.children) > 10 || getChildrenCount(token.children) > 5;
|
const isLong = sumTextsLength(token.children) > 10 || countNodesF(token.children) > 5;
|
||||||
const isMany = bigCount > 3;
|
const isMany = bigCount > 3;
|
||||||
return (createElement as any)('strong', {
|
return (createElement as any)('strong', {
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -122,7 +111,7 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
|
|
||||||
case 'motion': {
|
case 'motion': {
|
||||||
motionCount++;
|
motionCount++;
|
||||||
const isLong = getTextCount(token.children) > 10 || getChildrenCount(token.children) > 5;
|
const isLong = sumTextsLength(token.children) > 10 || countNodesF(token.children) > 5;
|
||||||
const isMany = motionCount > 3;
|
const isMany = motionCount > 3;
|
||||||
return (createElement as any)('span', {
|
return (createElement as any)('span', {
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -139,7 +128,7 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
return [createElement(MkUrl, {
|
return [createElement(MkUrl, {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
props: {
|
props: {
|
||||||
url: token.props.url,
|
url: token.node.props.url,
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
style: 'color:var(--mfmLink);'
|
style: 'color:var(--mfmLink);'
|
||||||
}
|
}
|
||||||
@@ -150,9 +139,9 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
return [createElement('a', {
|
return [createElement('a', {
|
||||||
attrs: {
|
attrs: {
|
||||||
class: 'link',
|
class: 'link',
|
||||||
href: token.props.url,
|
href: token.node.props.url,
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
title: token.props.url,
|
title: token.node.props.url,
|
||||||
style: 'color:var(--mfmLink);'
|
style: 'color:var(--mfmLink);'
|
||||||
}
|
}
|
||||||
}, genEl(token.children))];
|
}, genEl(token.children))];
|
||||||
@@ -162,8 +151,8 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
return [createElement(MkMention, {
|
return [createElement(MkMention, {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
props: {
|
props: {
|
||||||
host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host,
|
host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host,
|
||||||
username: token.props.username
|
username: token.node.props.username
|
||||||
}
|
}
|
||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
@@ -172,10 +161,10 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
return [createElement('router-link', {
|
return [createElement('router-link', {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
attrs: {
|
attrs: {
|
||||||
to: `/tags/${encodeURIComponent(token.props.hashtag)}`,
|
to: `/tags/${encodeURIComponent(token.node.props.hashtag)}`,
|
||||||
style: 'color:var(--mfmHashtag);'
|
style: 'color:var(--mfmHashtag);'
|
||||||
}
|
}
|
||||||
}, `#${token.props.hashtag}`)];
|
}, `#${token.node.props.hashtag}`)];
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'blockCode': {
|
case 'blockCode': {
|
||||||
@@ -184,7 +173,7 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
}, [
|
}, [
|
||||||
createElement('code', {
|
createElement('code', {
|
||||||
domProps: {
|
domProps: {
|
||||||
innerHTML: syntaxHighlight(token.props.code)
|
innerHTML: syntaxHighlight(token.node.props.code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
])];
|
])];
|
||||||
@@ -193,7 +182,7 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
case 'inlineCode': {
|
case 'inlineCode': {
|
||||||
return [createElement('code', {
|
return [createElement('code', {
|
||||||
domProps: {
|
domProps: {
|
||||||
innerHTML: syntaxHighlight(token.props.code)
|
innerHTML: syntaxHighlight(token.node.props.code)
|
||||||
}
|
}
|
||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
@@ -227,8 +216,8 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
return [createElement('mk-emoji', {
|
return [createElement('mk-emoji', {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
attrs: {
|
attrs: {
|
||||||
emoji: token.props.emoji,
|
emoji: token.node.props.emoji,
|
||||||
name: token.props.name
|
name: token.node.props.name
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
customEmojis: this.customEmojis || customEmojis,
|
customEmojis: this.customEmojis || customEmojis,
|
||||||
@@ -242,7 +231,7 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
return [createElement(MkFormula, {
|
return [createElement(MkFormula, {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
props: {
|
props: {
|
||||||
formula: token.props.formula
|
formula: token.node.props.formula
|
||||||
}
|
}
|
||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
@@ -252,13 +241,13 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
return [createElement(MkGoogle, {
|
return [createElement(MkGoogle, {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
props: {
|
props: {
|
||||||
q: token.props.query
|
q: token.node.props.query
|
||||||
}
|
}
|
||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
console.log('unknown ast type:', token.name);
|
console.log('unknown ast type:', token.node.type);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</ui-input>
|
</ui-input>
|
||||||
|
|
||||||
<ui-input v-model="birthday" type="date">
|
<ui-input v-model="birthday" type="date">
|
||||||
<span>{{ $t('birthday') }}</span>
|
<span slot="title">{{ $t('birthday') }}</span>
|
||||||
<span slot="prefix"><fa icon="birthday-cake"/></span>
|
<span slot="prefix"><fa icon="birthday-cake"/></span>
|
||||||
</ui-input>
|
</ui-input>
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
anime(reaction: string) {
|
anime(reaction: string) {
|
||||||
if (this.$store.state.device.reduceMotion) return;
|
if (this.$store.state.device.reduceMotion) return;
|
||||||
|
if (document.hidden) return;
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const rect = this.$refs[reaction].$el.getBoundingClientRect();
|
const rect = this.$refs[reaction].$el.getBoundingClientRect();
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { toUnicode } from 'punycode';
|
|||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('common/views/components/signin.vue'),
|
i18n: i18n('common/views/components/signin.vue'),
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
withAvatar: {
|
withAvatar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -33,6 +34,7 @@ export default Vue.extend({
|
|||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
signing: false,
|
signing: false,
|
||||||
@@ -45,11 +47,13 @@ export default Vue.extend({
|
|||||||
meta: null
|
meta: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.$root.getMeta().then(meta => {
|
this.$root.getMeta().then(meta => {
|
||||||
this.meta = meta;
|
this.meta = meta;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onUsernameChange() {
|
onUsernameChange() {
|
||||||
this.$root.api('users/show', {
|
this.$root.api('users/show', {
|
||||||
@@ -60,6 +64,7 @@ export default Vue.extend({
|
|||||||
this.user = null;
|
this.user = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.signing = true;
|
this.signing = true;
|
||||||
|
|
||||||
@@ -80,8 +85,6 @@ export default Vue.extend({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
|
|
||||||
|
|
||||||
.mk-signin
|
.mk-signin
|
||||||
color #555
|
color #555
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { toUnicode } from 'punycode';
|
|||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('common/views/components/signup.vue'),
|
i18n: i18n('common/views/components/signup.vue'),
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
host: toUnicode(host),
|
host: toUnicode(host),
|
||||||
@@ -64,6 +65,7 @@ export default Vue.extend({
|
|||||||
meta: null
|
meta: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
shouldShowProfileUrl(): boolean {
|
shouldShowProfileUrl(): boolean {
|
||||||
return (this.username != '' &&
|
return (this.username != '' &&
|
||||||
@@ -72,17 +74,20 @@ export default Vue.extend({
|
|||||||
this.usernameState != 'max-range');
|
this.usernameState != 'max-range');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.$root.getMeta().then(meta => {
|
this.$root.getMeta().then(meta => {
|
||||||
this.meta = meta;
|
this.meta = meta;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
const head = document.getElementsByTagName('head')[0];
|
const head = document.getElementsByTagName('head')[0];
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js');
|
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js');
|
||||||
head.appendChild(script);
|
head.appendChild(script);
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onChangeUsername() {
|
onChangeUsername() {
|
||||||
if (this.username == '') {
|
if (this.username == '') {
|
||||||
@@ -111,6 +116,7 @@ export default Vue.extend({
|
|||||||
this.usernameState = 'error';
|
this.usernameState = 'error';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onChangePassword() {
|
onChangePassword() {
|
||||||
if (this.password == '') {
|
if (this.password == '') {
|
||||||
this.passwordStrength = '';
|
this.passwordStrength = '';
|
||||||
@@ -120,6 +126,7 @@ export default Vue.extend({
|
|||||||
const strength = getPasswordStrength(this.password);
|
const strength = getPasswordStrength(this.password);
|
||||||
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||||
},
|
},
|
||||||
|
|
||||||
onChangePasswordRetype() {
|
onChangePasswordRetype() {
|
||||||
if (this.retypedPassword == '') {
|
if (this.retypedPassword == '') {
|
||||||
this.passwordRetypeState = null;
|
this.passwordRetypeState = null;
|
||||||
@@ -128,6 +135,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match';
|
this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match';
|
||||||
},
|
},
|
||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.$root.api('signup', {
|
this.$root.api('signup', {
|
||||||
username: this.username,
|
username: this.username,
|
||||||
@@ -138,8 +146,9 @@ export default Vue.extend({
|
|||||||
this.$root.api('signin', {
|
this.$root.api('signin', {
|
||||||
username: this.username,
|
username: this.username,
|
||||||
password: this.password
|
password: this.password
|
||||||
}, true).then(() => {
|
}, true).then(res => {
|
||||||
location.href = '/';
|
localStorage.setItem('i', res.i);
|
||||||
|
location.reload();
|
||||||
});
|
});
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
alert(this.$t('some-error'));
|
alert(this.$t('some-error'));
|
||||||
@@ -154,8 +163,6 @@ export default Vue.extend({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
|
|
||||||
|
|
||||||
.mk-signup
|
.mk-signup
|
||||||
min-width 302px
|
min-width 302px
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -33,14 +33,7 @@ export default Vue.extend({
|
|||||||
return typeof this.time == 'string' ? new Date(this.time) : this.time;
|
return typeof this.time == 'string' ? new Date(this.time) : this.time;
|
||||||
},
|
},
|
||||||
absolute(): string {
|
absolute(): string {
|
||||||
const time = this._time;
|
return this._time.toLocaleString();
|
||||||
return (
|
|
||||||
time.getFullYear() + '年' +
|
|
||||||
(time.getMonth() + 1) + '月' +
|
|
||||||
time.getDate() + '日' +
|
|
||||||
' ' +
|
|
||||||
time.getHours() + '時' +
|
|
||||||
time.getMinutes() + '分');
|
|
||||||
},
|
},
|
||||||
relative(): string {
|
relative(): string {
|
||||||
const time = this._time;
|
const time = this._time;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<div class="value" ref="passwordMetar"></div>
|
<div class="value" ref="passwordMetar"></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="label" ref="label"><slot></slot></span>
|
<span class="label" ref="label"><slot></slot></span>
|
||||||
|
<span class="title" ref="title"><slot name="title"></slot></span>
|
||||||
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
|
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
|
||||||
<template v-if="type != 'file'">
|
<template v-if="type != 'file'">
|
||||||
<input ref="input"
|
<input ref="input"
|
||||||
@@ -281,6 +282,20 @@ root(fill)
|
|||||||
transform-origin top left
|
transform-origin top left
|
||||||
transform scale(1)
|
transform scale(1)
|
||||||
|
|
||||||
|
> .title
|
||||||
|
position absolute
|
||||||
|
z-index 1
|
||||||
|
top fill ? -24px : -17px
|
||||||
|
left 0 !important
|
||||||
|
pointer-events none
|
||||||
|
font-size 16px
|
||||||
|
line-height 32px
|
||||||
|
color var(--inputLabel)
|
||||||
|
pointer-events none
|
||||||
|
//will-change transform
|
||||||
|
transform-origin top left
|
||||||
|
transform scale(.75)
|
||||||
|
|
||||||
> input
|
> input
|
||||||
display block
|
display block
|
||||||
width 100%
|
width 100%
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<div class="cudqjmnl">
|
||||||
|
<ui-card>
|
||||||
|
<div slot="title"><fa :icon="faList"/> {{ list.title }}</div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button>
|
||||||
|
<ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
|
||||||
|
</section>
|
||||||
|
</ui-card>
|
||||||
|
|
||||||
|
<ui-card>
|
||||||
|
<div slot="title"><fa :icon="faUsers"/> {{ $t('users') }}</div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||||
|
<div class="phcqulfl" v-for="user in users">
|
||||||
|
<div>
|
||||||
|
<a :href="user | userPage">
|
||||||
|
<mk-avatar class="avatar" :user="user" :disable-link="true"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<header>
|
||||||
|
<b><mk-user-name :user="user"/></b>
|
||||||
|
<span class="username">@{{ user | acct }}</span>
|
||||||
|
</header>
|
||||||
|
<div>
|
||||||
|
<a @click="remove(user)">{{ $t('remove-user') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</sequential-entrance>
|
||||||
|
</section>
|
||||||
|
</ui-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import i18n from '../../../i18n';
|
||||||
|
import { faList, faICursor, faUsers } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
i18n: i18n('common/views/components/user-list-editor.vue'),
|
||||||
|
|
||||||
|
props: {
|
||||||
|
list: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
users: [],
|
||||||
|
faList, faICursor, faTrashAlt, faUsers
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.fetchUsers();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
fetchUsers() {
|
||||||
|
this.$root.api('users/show', {
|
||||||
|
userIds: this.list.userIds
|
||||||
|
}).then(users => {
|
||||||
|
this.users = users;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
rename() {
|
||||||
|
this.$root.dialog({
|
||||||
|
title: this.$t('rename'),
|
||||||
|
input: {
|
||||||
|
default: this.list.title
|
||||||
|
}
|
||||||
|
}).then(({ canceled, result: title }) => {
|
||||||
|
if (canceled) return;
|
||||||
|
this.$root.api('users/lists/update', {
|
||||||
|
listId: this.list.id,
|
||||||
|
title: title
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
del() {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'warning',
|
||||||
|
text: this.$t('delete-are-you-sure').replace('$1', this.list.title),
|
||||||
|
showCancelButton: true
|
||||||
|
}).then(({ canceled }) => {
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
this.$root.api('users/lists/delete', {
|
||||||
|
listId: this.list.id
|
||||||
|
}).then(() => {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'success',
|
||||||
|
text: this.$t('deleted')
|
||||||
|
});
|
||||||
|
}).catch(e => {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(user: any) {
|
||||||
|
this.$root.api('users/lists/pull', {
|
||||||
|
listId: this.list.id,
|
||||||
|
userId: user.id
|
||||||
|
}).then(() => {
|
||||||
|
this.fetchUsers();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.cudqjmnl
|
||||||
|
.phcqulfl
|
||||||
|
display flex
|
||||||
|
padding 16px 0
|
||||||
|
border-top solid 1px var(--faceDivider)
|
||||||
|
|
||||||
|
> div:first-child
|
||||||
|
> a
|
||||||
|
> .avatar
|
||||||
|
width 64px
|
||||||
|
height 64px
|
||||||
|
|
||||||
|
> div:last-child
|
||||||
|
flex 1
|
||||||
|
padding-left 16px
|
||||||
|
|
||||||
|
@media (max-width 500px)
|
||||||
|
font-size 14px
|
||||||
|
|
||||||
|
> header
|
||||||
|
> .username
|
||||||
|
margin-left 8px
|
||||||
|
opacity 0.7
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :author="note.user" :custom-emojis="note.emojis"/>
|
<misskey-flavored-markdown v-if="note.text" :text="note.cw != null ? note.cw : note.text" :author="note.user" :custom-emojis="note.emojis"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class Autocomplete {
|
|||||||
private opts: {
|
private opts: {
|
||||||
model: string;
|
model: string;
|
||||||
};
|
};
|
||||||
|
private opening: boolean;
|
||||||
|
|
||||||
private get text(): string {
|
private get text(): string {
|
||||||
return this.vm[this.opts.model];
|
return this.vm[this.opts.model];
|
||||||
@@ -48,6 +49,7 @@ class Autocomplete {
|
|||||||
this.textarea = textarea;
|
this.textarea = textarea;
|
||||||
this.vm = vm;
|
this.vm = vm;
|
||||||
this.opts = opts;
|
this.opts = opts;
|
||||||
|
this.opening = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,6 +130,8 @@ class Autocomplete {
|
|||||||
if (type != this.currentType) {
|
if (type != this.currentType) {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
if (this.opening) return;
|
||||||
|
this.opening = true;
|
||||||
this.currentType = type;
|
this.currentType = type;
|
||||||
|
|
||||||
//#region サジェストを表示すべき位置を計算
|
//#region サジェストを表示すべき位置を計算
|
||||||
@@ -143,6 +147,8 @@ class Autocomplete {
|
|||||||
this.suggestion.x = x;
|
this.suggestion.x = x;
|
||||||
this.suggestion.y = y;
|
this.suggestion.y = y;
|
||||||
this.suggestion.q = q;
|
this.suggestion.q = q;
|
||||||
|
|
||||||
|
this.opening = false;
|
||||||
} else {
|
} else {
|
||||||
const MkAutocomplete = await import('../components/autocomplete.vue').then(m => m.default);
|
const MkAutocomplete = await import('../components/autocomplete.vue').then(m => m.default);
|
||||||
|
|
||||||
@@ -162,6 +168,8 @@ class Autocomplete {
|
|||||||
|
|
||||||
// 要素追加
|
// 要素追加
|
||||||
document.body.appendChild(this.suggestion.$el);
|
document.body.appendChild(this.suggestion.$el);
|
||||||
|
|
||||||
|
this.opening = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
:disabled="followWait">
|
:disabled="followWait">
|
||||||
<template v-if="!followWait">
|
<template v-if="!followWait">
|
||||||
<template v-if="user.hasPendingFollowRequestFromYou && user.isLocked"><fa icon="hourglass-half"/> {{ $t('request-pending') }}</template>
|
<template v-if="user.hasPendingFollowRequestFromYou && user.isLocked"><fa icon="hourglass-half"/> {{ $t('request-pending') }}</template>
|
||||||
<template v-else-if="user.hasPendingFollowRequestFromYou && !user.isLocked"><fa icon="hourglass-start"/> {{ $t('follow-processing') }}</template>
|
<template v-else-if="user.hasPendingFollowRequestFromYou && !user.isLocked"><fa icon="spinner"/> {{ $t('follow-processing') }}</template>
|
||||||
<template v-else-if="user.isFollowing"><fa icon="minus"/> {{ $t('following') }}</template>
|
<template v-else-if="user.isFollowing"><fa icon="minus"/> {{ $t('following') }}</template>
|
||||||
<template v-else-if="!user.isFollowing && user.isLocked"><fa icon="plus"/> {{ $t('follow-request') }}</template>
|
<template v-else-if="!user.isFollowing && user.isLocked"><fa icon="plus"/> {{ $t('follow-request') }}</template>
|
||||||
<template v-else-if="!user.isFollowing && !user.isLocked"><fa icon="plus"/> {{ $t('follow') }}</template>
|
<template v-else-if="!user.isFollowing && !user.isLocked"><fa icon="plus"/> {{ $t('follow') }}</template>
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ import { host } from '../../../config';
|
|||||||
import { erase, unique } from '../../../../../prelude/array';
|
import { erase, unique } from '../../../../../prelude/array';
|
||||||
import { length } from 'stringz';
|
import { length } from 'stringz';
|
||||||
import { toASCII } from 'punycode';
|
import { toASCII } from 'punycode';
|
||||||
|
import extractMentions from '../../../../../misc/extract-mentions';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('desktop/views/components/post-form.vue'),
|
i18n: i18n('desktop/views/components/post-form.vue'),
|
||||||
@@ -184,8 +185,7 @@ export default Vue.extend({
|
|||||||
if (this.reply && this.reply.text != null) {
|
if (this.reply && this.reply.text != null) {
|
||||||
const ast = parse(this.reply.text);
|
const ast = parse(this.reply.text);
|
||||||
|
|
||||||
// TODO: 新しいMFMパーサに対応
|
for (const x of extractMentions(ast)) {
|
||||||
for (const x of ast.filter(t => t.type == 'mention')) {
|
|
||||||
const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
|
const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
|
||||||
|
|
||||||
// 自分は除外
|
// 自分は除外
|
||||||
@@ -207,9 +207,8 @@ export default Vue.extend({
|
|||||||
this.visibility = this.reply.visibility;
|
this.visibility = this.reply.visibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ダイレクトへのリプライはリプライ先ユーザーを初期設定
|
if (this.reply) {
|
||||||
if (this.reply && this.reply.visibility === 'specified') {
|
this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
|
||||||
this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
|
|
||||||
this.visibleUsers.push(user);
|
this.visibleUsers.push(user);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -236,7 +235,7 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
trimmedLength(text: string) {
|
trimmedLength(text: string) {
|
||||||
return length(text.trim());
|
return length(text.trim());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import MkUserListsWindow from './user-lists-window.vue';
|
import MkUserListsWindow from './user-lists-window.vue';
|
||||||
|
import MkUserListWindow from './user-list-window.vue';
|
||||||
import MkFollowRequestsWindow from './received-follow-requests-window.vue';
|
import MkFollowRequestsWindow from './received-follow-requests-window.vue';
|
||||||
import MkSettingsWindow from './settings-window.vue';
|
import MkSettingsWindow from './settings-window.vue';
|
||||||
import MkDriveWindow from './drive-window.vue';
|
import MkDriveWindow from './drive-window.vue';
|
||||||
@@ -143,7 +144,9 @@ export default Vue.extend({
|
|||||||
this.close();
|
this.close();
|
||||||
const w = this.$root.new(MkUserListsWindow);
|
const w = this.$root.new(MkUserListsWindow);
|
||||||
w.$once('choosen', list => {
|
w.$once('choosen', list => {
|
||||||
this.$router.push(`i/lists/${ list.id }`);
|
this.$root.new(MkUserListWindow, {
|
||||||
|
list
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
followRequests() {
|
followRequests() {
|
||||||
|
|||||||
@@ -14,16 +14,34 @@ export default Vue.extend({
|
|||||||
i18n: i18n('desktop/views/components/ui.header.search.vue'),
|
i18n: i18n('desktop/views/components/ui.header.search.vue'),
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
q: ''
|
q: '',
|
||||||
|
wait: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
|
if (this.wait) return;
|
||||||
|
|
||||||
const q = this.q.trim();
|
const q = this.q.trim();
|
||||||
if (q.startsWith('@')) {
|
if (q.startsWith('@')) {
|
||||||
this.$router.push(`/${q}`);
|
this.$router.push(`/${q}`);
|
||||||
} else if (q.startsWith('#')) {
|
} else if (q.startsWith('#')) {
|
||||||
this.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
|
this.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
|
||||||
|
} else if (q.startsWith('https://')) {
|
||||||
|
this.wait = true;
|
||||||
|
try {
|
||||||
|
const res = await this.$root.api('ap/show', {
|
||||||
|
uri: q
|
||||||
|
});
|
||||||
|
if (res.type == 'User') {
|
||||||
|
this.$router.push(`/@${res.object.username}@${res.object.host}`);
|
||||||
|
} else if (res.type == 'Note') {
|
||||||
|
this.$router.push(`/notes/${res.object.id}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
this.wait = false;
|
||||||
} else {
|
} else {
|
||||||
this.$router.push(`/search?q=${encodeURIComponent(q)}`);
|
this.$router.push(`/search?q=${encodeURIComponent(q)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<mk-window ref="window" width="450px" height="500px" @closed="destroyDom">
|
||||||
|
<span slot="header"><fa icon="list"/> {{ list.title }}</span>
|
||||||
|
|
||||||
|
<x-editor :list="list"/>
|
||||||
|
</mk-window>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import XEditor from '../../../common/views/components/user-list-editor.vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
XEditor
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
list: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
|
<mk-window ref="window" width="450px" height="500px" @closed="destroyDom">
|
||||||
<span slot="header"><fa icon="list"/> {{ $t('title') }}</span>
|
<span slot="header"><fa icon="list"/> {{ $t('title') }}</span>
|
||||||
|
|
||||||
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp">
|
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp">
|
||||||
|
|||||||
@@ -627,6 +627,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
> .content
|
> .content
|
||||||
height 100%
|
height 100%
|
||||||
|
overflow auto
|
||||||
|
|
||||||
&:not([flexible])
|
&:not([flexible])
|
||||||
> .main > .body > .content
|
> .main > .body > .content
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<mk-error v-if="!fetching && requestInitPromise != null" @retry="resolveInitPromise"/>
|
<mk-error v-if="!fetching && requestInitPromise != null" @retry="resolveInitPromise"/>
|
||||||
|
|
||||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||||
<transition-group name="mk-notes" class="transition notes" ref="notes" tag="div">
|
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition notes" ref="notes" tag="div">
|
||||||
<template v-for="(note, i) in _notes">
|
<template v-for="(note, i) in _notes">
|
||||||
<x-note
|
<x-note
|
||||||
:note="note"
|
:note="note"
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span>
|
<span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</transition-group>
|
</component>
|
||||||
|
|
||||||
<footer v-if="more">
|
<footer v-if="more">
|
||||||
<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<header :class="$style.header">
|
<header :class="$style.header">
|
||||||
<h1>#{{ $route.params.tag }}</h1>
|
<h1>#{{ $route.params.tag }}</h1>
|
||||||
</header>
|
</header>
|
||||||
<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q }) }}</p>
|
<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q: $route.params.tag }) }}</p>
|
||||||
<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
|
<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
|
||||||
</mk-ui>
|
</mk-ui>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -24,9 +24,14 @@ export default Vue.extend({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
const image = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif'
|
||||||
|
];
|
||||||
this.$root.api('users/notes', {
|
this.$root.api('users/notes', {
|
||||||
userId: this.user.id,
|
userId: this.user.id,
|
||||||
withFiles: true,
|
fileType: image,
|
||||||
limit: 9,
|
limit: 9,
|
||||||
untilDate: new Date().getTime() + 1000 * 86400 * 365
|
untilDate: new Date().getTime() + 1000 * 86400 * 365
|
||||||
}).then(notes => {
|
}).then(notes => {
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ import {
|
|||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faMapMarker,
|
faMapMarker,
|
||||||
faRobot,
|
faRobot,
|
||||||
|
faHourglassHalf,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -253,6 +254,7 @@ library.add(
|
|||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faMapMarker,
|
faMapMarker,
|
||||||
faRobot,
|
faRobot,
|
||||||
|
faHourglassHalf,
|
||||||
|
|
||||||
farBell,
|
farBell,
|
||||||
farEnvelope,
|
farEnvelope,
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import { host } from '../../../config';
|
|||||||
import { erase, unique } from '../../../../../prelude/array';
|
import { erase, unique } from '../../../../../prelude/array';
|
||||||
import { length } from 'stringz';
|
import { length } from 'stringz';
|
||||||
import { toASCII } from 'punycode';
|
import { toASCII } from 'punycode';
|
||||||
|
import extractMentions from '../../../../../misc/extract-mentions';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('mobile/views/components/post-form.vue'),
|
i18n: i18n('mobile/views/components/post-form.vue'),
|
||||||
@@ -174,7 +175,7 @@ export default Vue.extend({
|
|||||||
if (this.reply && this.reply.text != null) {
|
if (this.reply && this.reply.text != null) {
|
||||||
const ast = parse(this.reply.text);
|
const ast = parse(this.reply.text);
|
||||||
|
|
||||||
for (const x of ast.filter(t => t.type == 'mention')) {
|
for (const x of extractMentions(ast)) {
|
||||||
const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
|
const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
|
||||||
|
|
||||||
// 自分は除外
|
// 自分は除外
|
||||||
@@ -196,9 +197,8 @@ export default Vue.extend({
|
|||||||
this.visibility = this.reply.visibility;
|
this.visibility = this.reply.visibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ダイレクトへのリプライはリプライ先ユーザーを初期設定
|
if (this.reply) {
|
||||||
if (this.reply && this.reply.visibility === 'specified') {
|
this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
|
||||||
this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
|
|
||||||
this.visibleUsers.push(user);
|
this.visibleUsers.push(user);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ export default Vue.extend({
|
|||||||
hasGameInvitation: false,
|
hasGameInvitation: false,
|
||||||
connection: null,
|
connection: null,
|
||||||
aboutUrl: `/docs/${lang}/about`,
|
aboutUrl: `/docs/${lang}/about`,
|
||||||
announcements: []
|
announcements: [],
|
||||||
|
searching: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -95,17 +96,34 @@ export default Vue.extend({
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
search() {
|
search() {
|
||||||
|
if (this.searching) return;
|
||||||
|
|
||||||
this.$root.dialog({
|
this.$root.dialog({
|
||||||
title: this.$t('search'),
|
title: this.$t('search'),
|
||||||
input: true
|
input: true
|
||||||
}).then(({ canceled, result: query }) => {
|
}).then(async ({ canceled, result: query }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
const q = query.trim();
|
const q = this.q.trim();
|
||||||
if (q.startsWith('@')) {
|
if (q.startsWith('@')) {
|
||||||
this.$router.push(`/${q}`);
|
this.$router.push(`/${q}`);
|
||||||
} else if (q.startsWith('#')) {
|
} else if (q.startsWith('#')) {
|
||||||
this.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
|
this.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
|
||||||
|
} else if (q.startsWith('https://')) {
|
||||||
|
this.searching = true;
|
||||||
|
try {
|
||||||
|
const res = await this.$root.api('ap/show', {
|
||||||
|
uri: q
|
||||||
|
});
|
||||||
|
if (res.type == 'User') {
|
||||||
|
this.$router.push(`/@${res.object.username}@${res.object.host}`);
|
||||||
|
} else if (res.type == 'Note') {
|
||||||
|
this.$router.push(`/notes/${res.object.id}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
this.searching = false;
|
||||||
} else {
|
} else {
|
||||||
this.$router.push(`/search?q=${encodeURIComponent(q)}`);
|
this.$router.push(`/search?q=${encodeURIComponent(q)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<span slot="header"><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</span>
|
<span slot="header"><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</span>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<p v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q }) }}</p>
|
<p v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q: $route.params.tag }) }}</p>
|
||||||
<mk-notes ref="timeline" :more="existMore ? more : null"/>
|
<mk-notes ref="timeline" :more="existMore ? more : null"/>
|
||||||
</main>
|
</main>
|
||||||
</mk-ui>
|
</mk-ui>
|
||||||
|
|||||||
@@ -3,11 +3,7 @@
|
|||||||
<span slot="header" v-if="!fetching"><fa icon="list"/>{{ list.title }}</span>
|
<span slot="header" v-if="!fetching"><fa icon="list"/>{{ list.title }}</span>
|
||||||
|
|
||||||
<main v-if="!fetching">
|
<main v-if="!fetching">
|
||||||
<ul>
|
<x-editor :list="list"/>
|
||||||
<li v-for="user in users" :key="user.id"><router-link :to="user | userPage">
|
|
||||||
<mk-user-name :user="user"/>
|
|
||||||
</router-link></li>
|
|
||||||
</ul>
|
|
||||||
</main>
|
</main>
|
||||||
</mk-ui>
|
</mk-ui>
|
||||||
</template>
|
</template>
|
||||||
@@ -15,13 +11,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Progress from '../../../common/scripts/loading';
|
import Progress from '../../../common/scripts/loading';
|
||||||
|
import XEditor from '../../../common/views/components/user-list-editor.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
XEditor
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
fetching: true,
|
fetching: true,
|
||||||
list: null,
|
list: null
|
||||||
users: null
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -42,12 +41,6 @@ export default Vue.extend({
|
|||||||
this.fetching = false;
|
this.fetching = false;
|
||||||
|
|
||||||
Progress.done();
|
Progress.done();
|
||||||
|
|
||||||
this.$root.api('users/show', {
|
|
||||||
userIds: this.list.userIds
|
|
||||||
}).then(users => {
|
|
||||||
this.users = users;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,8 +48,6 @@ export default Vue.extend({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
|
|
||||||
|
|
||||||
main
|
main
|
||||||
width 100%
|
width 100%
|
||||||
max-width 680px
|
max-width 680px
|
||||||
|
|||||||
@@ -26,10 +26,15 @@ export default Vue.extend({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
const image = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif'
|
||||||
|
];
|
||||||
this.$root.api('users/notes', {
|
this.$root.api('users/notes', {
|
||||||
userId: this.user.id,
|
userId: this.user.id,
|
||||||
withFiles: true,
|
fileType: image,
|
||||||
limit: 6,
|
limit: 9,
|
||||||
untilDate: new Date().getTime() + 1000 * 86400 * 365
|
untilDate: new Date().getTime() + 1000 * 86400 * 365
|
||||||
}).then(notes => {
|
}).then(notes => {
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
declare module "*.vue" {
|
declare module '*.vue' {
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
export default Vue;
|
export default Vue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,21 +39,7 @@ export type Source = {
|
|||||||
|
|
||||||
accesslog?: string;
|
accesslog?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Service Worker
|
|
||||||
*/
|
|
||||||
sw?: {
|
|
||||||
public_key: string;
|
|
||||||
private_key: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
clusterLimit?: number;
|
clusterLimit?: number;
|
||||||
|
|
||||||
user_recommendation?: {
|
|
||||||
external: boolean;
|
|
||||||
engine: string;
|
|
||||||
timeout: number;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+26
-62
@@ -1,4 +1,4 @@
|
|||||||
import { count, concat } from "../../prelude/array";
|
import { count, concat } from '../../prelude/array';
|
||||||
|
|
||||||
// MISSKEY REVERSI ENGINE
|
// MISSKEY REVERSI ENGINE
|
||||||
|
|
||||||
@@ -76,27 +76,14 @@ export default class Reversi {
|
|||||||
this.mapHeight = map.length;
|
this.mapHeight = map.length;
|
||||||
const mapData = map.join('');
|
const mapData = map.join('');
|
||||||
|
|
||||||
this.board = mapData.split('').map(d => {
|
this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined);
|
||||||
if (d == '-') return null;
|
|
||||||
if (d == 'b') return BLACK;
|
|
||||||
if (d == 'w') return WHITE;
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.map = mapData.split('').map(d => {
|
this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null');
|
||||||
if (d == '-' || d == 'b' || d == 'w') return 'empty';
|
|
||||||
return 'null';
|
|
||||||
});
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
|
// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
|
||||||
if (!this.canPutSomewhere(BLACK)) {
|
if (!this.canPutSomewhere(BLACK))
|
||||||
if (!this.canPutSomewhere(WHITE)) {
|
this.turn = this.canPutSomewhere(WHITE) ? WHITE : null;
|
||||||
this.turn = null;
|
|
||||||
} else {
|
|
||||||
this.turn = WHITE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,16 +104,14 @@ export default class Reversi {
|
|||||||
* 黒石の比率
|
* 黒石の比率
|
||||||
*/
|
*/
|
||||||
public get blackP() {
|
public get blackP() {
|
||||||
if (this.blackCount == 0 && this.whiteCount == 0) return 0;
|
return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.blackCount / (this.blackCount + this.whiteCount);
|
||||||
return this.blackCount / (this.blackCount + this.whiteCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 白石の比率
|
* 白石の比率
|
||||||
*/
|
*/
|
||||||
public get whiteP() {
|
public get whiteP() {
|
||||||
if (this.blackCount == 0 && this.whiteCount == 0) return 0;
|
return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.whiteCount / (this.blackCount + this.whiteCount);
|
||||||
return this.whiteCount / (this.blackCount + this.whiteCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public transformPosToXy(pos: number): number[] {
|
public transformPosToXy(pos: number): number[] {
|
||||||
@@ -172,13 +157,10 @@ export default class Reversi {
|
|||||||
|
|
||||||
private calcTurn() {
|
private calcTurn() {
|
||||||
// ターン計算
|
// ターン計算
|
||||||
if (this.canPutSomewhere(!this.prevColor)) {
|
this.turn =
|
||||||
this.turn = !this.prevColor;
|
this.canPutSomewhere(!this.prevColor) ? !this.prevColor :
|
||||||
} else if (this.canPutSomewhere(this.prevColor)) {
|
this.canPutSomewhere(this.prevColor) ? this.prevColor :
|
||||||
this.turn = this.prevColor;
|
null;
|
||||||
} else {
|
|
||||||
this.turn = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public undo() {
|
public undo() {
|
||||||
@@ -199,8 +181,7 @@ export default class Reversi {
|
|||||||
*/
|
*/
|
||||||
public mapDataGet(pos: number): MapPixel {
|
public mapDataGet(pos: number): MapPixel {
|
||||||
const [x, y] = this.transformPosToXy(pos);
|
const [x, y] = this.transformPosToXy(pos);
|
||||||
if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) return 'null';
|
return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos];
|
||||||
return this.map[pos];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,16 +204,10 @@ export default class Reversi {
|
|||||||
* @param pos 位置
|
* @param pos 位置
|
||||||
*/
|
*/
|
||||||
public canPut(color: Color, pos: number): boolean {
|
public canPut(color: Color, pos: number): boolean {
|
||||||
// 既に石が置いてある場所には打てない
|
return (
|
||||||
if (this.board[pos] !== null) return false;
|
this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない
|
||||||
|
this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード
|
||||||
if (this.opts.canPutEverywhere) {
|
this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか
|
||||||
// 挟んでなくても置けるモード
|
|
||||||
return this.mapDataGet(pos) == 'empty';
|
|
||||||
} else {
|
|
||||||
// 相手の石を1つでも反転させられるか
|
|
||||||
return this.effects(color, pos).length !== 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -263,19 +238,13 @@ export default class Reversi {
|
|||||||
[x, y] = nextPos(x, y);
|
[x, y] = nextPos(x, y);
|
||||||
|
|
||||||
// 座標が指し示す位置がボード外に出たとき
|
// 座標が指し示す位置がボード外に出たとき
|
||||||
if (this.opts.loopedBoard) {
|
if (this.opts.loopedBoard && this.transformXyToPos(
|
||||||
x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth;
|
(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth),
|
||||||
y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight;
|
(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) == initPos)
|
||||||
|
|
||||||
if (this.transformXyToPos(x, y) == initPos) {
|
|
||||||
// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
|
// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
|
||||||
return found;
|
return found;
|
||||||
}
|
else if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight)
|
||||||
} else {
|
return []; // 挟めないことが確定 (盤面外に到達)
|
||||||
if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) {
|
|
||||||
return []; // 挟めないことが確定 (盤面外に到達)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pos = this.transformXyToPos(x, y);
|
const pos = this.transformXyToPos(x, y);
|
||||||
if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達)
|
if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達)
|
||||||
@@ -300,14 +269,9 @@ export default class Reversi {
|
|||||||
* ゲームの勝者 (null = 引き分け)
|
* ゲームの勝者 (null = 引き分け)
|
||||||
*/
|
*/
|
||||||
public get winner(): Color {
|
public get winner(): Color {
|
||||||
if (!this.isEnded) return undefined;
|
return this.isEnded ?
|
||||||
|
this.blackCount == this.whiteCount ? null :
|
||||||
if (this.blackCount == this.whiteCount) return null;
|
this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK :
|
||||||
|
undefined;
|
||||||
if (this.opts.isLlotheo) {
|
|
||||||
return this.blackCount > this.whiteCount ? WHITE : BLACK;
|
|
||||||
} else {
|
|
||||||
return this.blackCount > this.whiteCount ? BLACK : WHITE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-18
@@ -2,10 +2,10 @@ const jsdom = require('jsdom');
|
|||||||
const { JSDOM } = jsdom;
|
const { JSDOM } = jsdom;
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { INote } from '../models/note';
|
import { INote } from '../models/note';
|
||||||
import { Node } from './parser';
|
|
||||||
import { intersperse } from '../prelude/array';
|
import { intersperse } from '../prelude/array';
|
||||||
|
import { MfmForest, MfmTree } from './parser';
|
||||||
|
|
||||||
export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) => {
|
export default (tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) => {
|
||||||
if (tokens == null) {
|
if (tokens == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -14,11 +14,11 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser
|
|||||||
|
|
||||||
const doc = window.document;
|
const doc = window.document;
|
||||||
|
|
||||||
function appendChildren(children: Node[], targetElement: any): void {
|
function appendChildren(children: MfmForest, targetElement: any): void {
|
||||||
for (const child of children.map(n => handlers[n.name](n))) targetElement.appendChild(child);
|
for (const child of children.map(t => handlers[t.node.type](t))) targetElement.appendChild(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlers: { [key: string]: (token: Node) => any } = {
|
const handlers: { [key: string]: (token: MfmTree) => any } = {
|
||||||
bold(token) {
|
bold(token) {
|
||||||
const el = doc.createElement('b');
|
const el = doc.createElement('b');
|
||||||
appendChildren(token.children, el);
|
appendChildren(token.children, el);
|
||||||
@@ -58,7 +58,7 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser
|
|||||||
blockCode(token) {
|
blockCode(token) {
|
||||||
const pre = doc.createElement('pre');
|
const pre = doc.createElement('pre');
|
||||||
const inner = doc.createElement('code');
|
const inner = doc.createElement('code');
|
||||||
inner.innerHTML = token.props.code;
|
inner.innerHTML = token.node.props.code;
|
||||||
pre.appendChild(inner);
|
pre.appendChild(inner);
|
||||||
return pre;
|
return pre;
|
||||||
},
|
},
|
||||||
@@ -70,39 +70,39 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser
|
|||||||
},
|
},
|
||||||
|
|
||||||
emoji(token) {
|
emoji(token) {
|
||||||
return doc.createTextNode(token.props.emoji ? token.props.emoji : `:${token.props.name}:`);
|
return doc.createTextNode(token.node.props.emoji ? token.node.props.emoji : `:${token.node.props.name}:`);
|
||||||
},
|
},
|
||||||
|
|
||||||
hashtag(token) {
|
hashtag(token) {
|
||||||
const a = doc.createElement('a');
|
const a = doc.createElement('a');
|
||||||
a.href = `${config.url}/tags/${token.props.hashtag}`;
|
a.href = `${config.url}/tags/${token.node.props.hashtag}`;
|
||||||
a.textContent = `#${token.props.hashtag}`;
|
a.textContent = `#${token.node.props.hashtag}`;
|
||||||
a.setAttribute('rel', 'tag');
|
a.setAttribute('rel', 'tag');
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
|
|
||||||
inlineCode(token) {
|
inlineCode(token) {
|
||||||
const el = doc.createElement('code');
|
const el = doc.createElement('code');
|
||||||
el.textContent = token.props.code;
|
el.textContent = token.node.props.code;
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
math(token) {
|
math(token) {
|
||||||
const el = doc.createElement('code');
|
const el = doc.createElement('code');
|
||||||
el.textContent = token.props.formula;
|
el.textContent = token.node.props.formula;
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
link(token) {
|
link(token) {
|
||||||
const a = doc.createElement('a');
|
const a = doc.createElement('a');
|
||||||
a.href = token.props.url;
|
a.href = token.node.props.url;
|
||||||
appendChildren(token.children, a);
|
appendChildren(token.children, a);
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
|
|
||||||
mention(token) {
|
mention(token) {
|
||||||
const a = doc.createElement('a');
|
const a = doc.createElement('a');
|
||||||
const { username, host, acct } = token.props;
|
const { username, host, acct } = token.node.props;
|
||||||
switch (host) {
|
switch (host) {
|
||||||
case 'github.com':
|
case 'github.com':
|
||||||
a.href = `https://github.com/${username}`;
|
a.href = `https://github.com/${username}`;
|
||||||
@@ -133,7 +133,7 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser
|
|||||||
|
|
||||||
text(token) {
|
text(token) {
|
||||||
const el = doc.createElement('span');
|
const el = doc.createElement('span');
|
||||||
const nodes = (token.props.text as string).split('\n').map(x => doc.createTextNode(x));
|
const nodes = (token.node.props.text as string).split('\n').map(x => doc.createTextNode(x));
|
||||||
|
|
||||||
for (const x of intersperse('br', nodes)) {
|
for (const x of intersperse('br', nodes)) {
|
||||||
el.appendChild(x === 'br' ? doc.createElement('br') : x);
|
el.appendChild(x === 'br' ? doc.createElement('br') : x);
|
||||||
@@ -144,15 +144,15 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser
|
|||||||
|
|
||||||
url(token) {
|
url(token) {
|
||||||
const a = doc.createElement('a');
|
const a = doc.createElement('a');
|
||||||
a.href = token.props.url;
|
a.href = token.node.props.url;
|
||||||
a.textContent = token.props.url;
|
a.textContent = token.node.props.url;
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
|
|
||||||
search(token) {
|
search(token) {
|
||||||
const a = doc.createElement('a');
|
const a = doc.createElement('a');
|
||||||
a.href = `https://www.google.com/?#q=${token.props.query}`;
|
a.href = `https://www.google.com/?#q=${token.node.props.query}`;
|
||||||
a.textContent = token.props.content;
|
a.textContent = token.node.props.content;
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+28
-32
@@ -1,40 +1,36 @@
|
|||||||
import parser, { Node, plainParser } from './parser';
|
import parser, { plainParser, MfmForest, MfmTree } from './parser';
|
||||||
import * as A from '../prelude/array';
|
import * as A from '../prelude/array';
|
||||||
import * as S from '../prelude/string';
|
import * as S from '../prelude/string';
|
||||||
|
import { createTree, createLeaf } from '../prelude/tree';
|
||||||
|
|
||||||
export default (source: string, plainText = false): Node[] => {
|
function concatTextTrees(ts: MfmForest): MfmTree {
|
||||||
|
return createLeaf({ type: 'text', props: { text: S.concat(ts.map(x => x.node.props.text)) } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function concatIfTextTrees(ts: MfmForest): MfmForest {
|
||||||
|
return ts[0].node.type === 'text' ? [concatTextTrees(ts)] : ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function concatConsecutiveTextTrees(ts: MfmForest): MfmForest {
|
||||||
|
const us = A.concat(A.groupOn(t => t.node.type, ts).map(concatIfTextTrees));
|
||||||
|
return us.map(t => createTree(t.node, concatConsecutiveTextTrees(t.children)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEmptyTextTree(t: MfmTree): boolean {
|
||||||
|
return t.node.type == 'text' && t.node.props.text === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeEmptyTextNodes(ts: MfmForest): MfmForest {
|
||||||
|
return ts
|
||||||
|
.filter(t => !isEmptyTextTree(t))
|
||||||
|
.map(t => createTree(t.node, removeEmptyTextNodes(t.children)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (source: string, plainText = false): MfmForest => {
|
||||||
if (source == null || source == '') {
|
if (source == null || source == '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let nodes: Node[] = plainText ? plainParser.root.tryParse(source) : parser.root.tryParse(source);
|
const raw = plainText ? plainParser.root.tryParse(source) : parser.root.tryParse(source) as MfmForest;
|
||||||
|
return removeEmptyTextNodes(concatConsecutiveTextTrees(raw));
|
||||||
const combineText = (es: Node[]): Node =>
|
|
||||||
({ name: 'text', props: { text: S.concat(es.map(e => e.props.text)) } });
|
|
||||||
|
|
||||||
const concatText = (nodes: Node[]): Node[] =>
|
|
||||||
A.concat(A.groupOn(x => x.name, nodes).map(es =>
|
|
||||||
es[0].name === 'text' ? [combineText(es)] : es
|
|
||||||
));
|
|
||||||
|
|
||||||
const concatTextRecursive = (es: Node[]): void => {
|
|
||||||
for (const x of es.filter(x => x.children)) {
|
|
||||||
x.children = concatText(x.children);
|
|
||||||
concatTextRecursive(x.children);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
nodes = concatText(nodes);
|
|
||||||
concatTextRecursive(nodes);
|
|
||||||
|
|
||||||
const removeEmptyTextNodes = (nodes: Node[]) => {
|
|
||||||
for (const n of nodes.filter(n => n.children)) {
|
|
||||||
n.children = removeEmptyTextNodes(n.children);
|
|
||||||
}
|
|
||||||
return nodes.filter(n => !(n.name == 'text' && n.props.text == ''));
|
|
||||||
};
|
|
||||||
|
|
||||||
nodes = removeEmptyTextNodes(nodes);
|
|
||||||
|
|
||||||
return nodes;
|
|
||||||
};
|
};
|
||||||
|
|||||||
+79
-79
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
|||||||
import { capitalize, toUpperCase } from "../prelude/string";
|
import { capitalize, toUpperCase } from '../prelude/string';
|
||||||
|
|
||||||
function escape(text: string) {
|
function escape(text: string) {
|
||||||
return text
|
return text
|
||||||
@@ -307,7 +307,7 @@ const elements: Element[] = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// specify lang is todo
|
// TODO: specify lang
|
||||||
export default (source: string, lang?: string): string => {
|
export default (source: string, lang?: string): string => {
|
||||||
let code = source;
|
let code = source;
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { EmojiNode, MfmForest } from '../mfm/parser';
|
||||||
|
import { preorderF } from '../prelude/tree';
|
||||||
|
import { unique } from '../prelude/array';
|
||||||
|
|
||||||
|
export default function(mfmForest: MfmForest): string[] {
|
||||||
|
const emojiNodes = preorderF(mfmForest).filter(x => x.type === 'emoji') as EmojiNode[];
|
||||||
|
const emojis = emojiNodes.filter(x => x.props.name && x.props.name.length <= 100).map(x => x.props.name);
|
||||||
|
return unique(emojis);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { HashtagNode, MfmForest } from '../mfm/parser';
|
||||||
|
import { preorderF } from '../prelude/tree';
|
||||||
|
import { unique } from '../prelude/array';
|
||||||
|
|
||||||
|
export default function(mfmForest: MfmForest): string[] {
|
||||||
|
const hashtagNodes = preorderF(mfmForest).filter(x => x.type === 'hashtag') as HashtagNode[];
|
||||||
|
const hashtags = hashtagNodes.map(x => x.props.hashtag);
|
||||||
|
return unique(hashtags);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// test is located in test/extract-mentions
|
||||||
|
|
||||||
|
import { MentionNode, MfmForest } from '../mfm/parser';
|
||||||
|
import { preorderF } from '../prelude/tree';
|
||||||
|
|
||||||
|
export default function(mfmForest: MfmForest): MentionNode['props'][] {
|
||||||
|
// TODO: 重複を削除
|
||||||
|
const mentionNodes = preorderF(mfmForest).filter(x => x.type === 'mention') as MentionNode[];
|
||||||
|
return mentionNodes.map(x => x.props);
|
||||||
|
}
|
||||||
@@ -17,9 +17,10 @@ const defaultMeta: any = {
|
|||||||
enableGithubIntegration: false,
|
enableGithubIntegration: false,
|
||||||
enableDiscordIntegration: false,
|
enableDiscordIntegration: false,
|
||||||
enableExternalUserRecommendation: false,
|
enableExternalUserRecommendation: false,
|
||||||
externalUserRecommendationEngine: "https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}",
|
externalUserRecommendationEngine: 'https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}',
|
||||||
externalUserRecommendationTimeout: 300000,
|
externalUserRecommendationTimeout: 300000,
|
||||||
errorImageUrl: 'https://ai.misskey.xyz/aiart/yubitun.png'
|
errorImageUrl: 'https://ai.misskey.xyz/aiart/yubitun.png',
|
||||||
|
enableServiceWorker: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function(): Promise<IMeta> {
|
export default async function(): Promise<IMeta> {
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ const summarize = (note: any): string => {
|
|||||||
let summary = '';
|
let summary = '';
|
||||||
|
|
||||||
// 本文
|
// 本文
|
||||||
summary += note.text ? note.text : '';
|
if (note.cw != null) {
|
||||||
|
summary += note.cw;
|
||||||
|
} else {
|
||||||
|
summary += note.text ? note.text : '';
|
||||||
|
}
|
||||||
|
|
||||||
// ファイルが添付されているとき
|
// ファイルが添付されているとき
|
||||||
if ((note.files || []).length != 0) {
|
if ((note.files || []).length != 0) {
|
||||||
|
|||||||
@@ -15,4 +15,6 @@ export type IEmoji = {
|
|||||||
url: string;
|
url: string;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
updatedAt?: Date;
|
updatedAt?: Date;
|
||||||
|
/** AP object id */
|
||||||
|
uri?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -138,6 +138,19 @@ if ((config as any).user_recommendation) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if ((config as any).sw) {
|
||||||
|
Meta.findOne({}).then(m => {
|
||||||
|
if (m != null && m.enableServiceWorker == null) {
|
||||||
|
Meta.update({}, {
|
||||||
|
$set: {
|
||||||
|
enableServiceWorker: true,
|
||||||
|
swPublicKey: (config as any).sw.public_key,
|
||||||
|
swPrivateKey: (config as any).sw.private_key
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export type IMeta = {
|
export type IMeta = {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -223,4 +236,8 @@ export type IMeta = {
|
|||||||
smtpPort?: number;
|
smtpPort?: number;
|
||||||
smtpUser?: string;
|
smtpUser?: string;
|
||||||
smtpPass?: string;
|
smtpPass?: string;
|
||||||
|
|
||||||
|
enableServiceWorker?: boolean;
|
||||||
|
swPublicKey?: string;
|
||||||
|
swPrivateKey?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -155,7 +155,7 @@ export const isRemoteUser = (user: any): user is IRemoteUser =>
|
|||||||
|
|
||||||
//#region Validators
|
//#region Validators
|
||||||
export function validateUsername(username: string, remote?: boolean): boolean {
|
export function validateUsername(username: string, remote?: boolean): boolean {
|
||||||
return typeof username == 'string' && (remote ? /^\w+([\w\.-]+\w+)?$/ : /^[a-zA-Z0-9_]{1,20}$/).test(username);
|
return typeof username == 'string' && (remote ? /^\w([\w-]*\w)?$/ : /^\w{1,20}$/).test(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validatePassword(password: string): boolean {
|
export function validatePassword(password: string): boolean {
|
||||||
|
|||||||
+51
-9
@@ -1,31 +1,53 @@
|
|||||||
export function countIf<T>(f: (x: T) => boolean, xs: T[]): number {
|
import { EndoRelation, Predicate } from './relation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the number of elements that satisfy the predicate
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function countIf<T>(f: Predicate<T>, xs: T[]): number {
|
||||||
return xs.filter(f).length;
|
return xs.filter(f).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function count<T>(x: T, xs: T[]): number {
|
/**
|
||||||
return countIf(y => x === y, xs);
|
* Count the number of elements that is equal to the element
|
||||||
|
*/
|
||||||
|
export function count<T>(a: T, xs: T[]): number {
|
||||||
|
return countIf(x => x === a, xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concatenate an array of arrays
|
||||||
|
*/
|
||||||
export function concat<T>(xss: T[][]): T[] {
|
export function concat<T>(xss: T[][]): T[] {
|
||||||
return ([] as T[]).concat(...xss);
|
return ([] as T[]).concat(...xss);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intersperse the element between the elements of the array
|
||||||
|
* @param sep The element to be interspersed
|
||||||
|
*/
|
||||||
export function intersperse<T>(sep: T, xs: T[]): T[] {
|
export function intersperse<T>(sep: T, xs: T[]): T[] {
|
||||||
return concat(xs.map(x => [sep, x])).slice(1);
|
return concat(xs.map(x => [sep, x])).slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function erase<T>(x: T, xs: T[]): T[] {
|
/**
|
||||||
return xs.filter(y => x !== y);
|
* Returns the array of elements that is not equal to the element
|
||||||
|
*/
|
||||||
|
export function erase<T>(a: T, xs: T[]): T[] {
|
||||||
|
return xs.filter(x => x !== a);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the array of all elements in the first array not contained in the second array.
|
* Finds the array of all elements in the first array not contained in the second array.
|
||||||
* The order of result values are determined by the first array.
|
* The order of result values are determined by the first array.
|
||||||
*/
|
*/
|
||||||
export function difference<T>(includes: T[], excludes: T[]): T[] {
|
export function difference<T>(xs: T[], ys: T[]): T[] {
|
||||||
return includes.filter(x => !excludes.includes(x));
|
return xs.filter(x => !ys.includes(x));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all but the first element from every group of equivalent elements
|
||||||
|
*/
|
||||||
export function unique<T>(xs: T[]): T[] {
|
export function unique<T>(xs: T[]): T[] {
|
||||||
return [...new Set(xs)];
|
return [...new Set(xs)];
|
||||||
}
|
}
|
||||||
@@ -38,7 +60,11 @@ export function maximum(xs: number[]): number {
|
|||||||
return Math.max(...xs);
|
return Math.max(...xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function groupBy<T>(f: (x: T, y: T) => boolean, xs: T[]): T[][] {
|
/**
|
||||||
|
* Splits an array based on the equivalence relation.
|
||||||
|
* The concatenation of the result is equal to the argument.
|
||||||
|
*/
|
||||||
|
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
|
||||||
const groups = [] as T[][];
|
const groups = [] as T[][];
|
||||||
for (const x of xs) {
|
for (const x of xs) {
|
||||||
if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) {
|
if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) {
|
||||||
@@ -50,10 +76,17 @@ export function groupBy<T>(f: (x: T, y: T) => boolean, xs: T[]): T[][] {
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits an array based on the equivalence relation induced by the function.
|
||||||
|
* The concatenation of the result is equal to the argument.
|
||||||
|
*/
|
||||||
export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
|
export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
|
||||||
return groupBy((a, b) => f(a) === f(b), xs);
|
return groupBy((a, b) => f(a) === f(b), xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two arrays by lexicographical order
|
||||||
|
*/
|
||||||
export function lessThan(xs: number[], ys: number[]): boolean {
|
export function lessThan(xs: number[], ys: number[]): boolean {
|
||||||
for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
|
for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
|
||||||
if (xs[i] < ys[i]) return true;
|
if (xs[i] < ys[i]) return true;
|
||||||
@@ -62,7 +95,10 @@ export function lessThan(xs: number[], ys: number[]): boolean {
|
|||||||
return xs.length < ys.length;
|
return xs.length < ys.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function takeWhile<T>(f: (x: T) => boolean, xs: T[]): T[] {
|
/**
|
||||||
|
* Returns the longest prefix of elements that satisfy the predicate
|
||||||
|
*/
|
||||||
|
export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
|
||||||
const ys = [];
|
const ys = [];
|
||||||
for (const x of xs) {
|
for (const x of xs) {
|
||||||
if (f(x)) {
|
if (f(x)) {
|
||||||
@@ -73,3 +109,9 @@ export function takeWhile<T>(f: (x: T) => boolean, xs: T[]): T[] {
|
|||||||
}
|
}
|
||||||
return ys;
|
return ys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function cumulativeSum(xs: number[]): number[] {
|
||||||
|
const ys = Array.from(xs); // deep copy
|
||||||
|
for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1];
|
||||||
|
return ys;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type Predicate<T> = (a: T) => boolean;
|
||||||
|
|
||||||
|
export type Relation<T, U> = (a: T, b: U) => boolean;
|
||||||
|
|
||||||
|
export type EndoRelation<T> = Relation<T, T>;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
export function concat(xs: string[]): string {
|
export function concat(xs: string[]): string {
|
||||||
return xs.reduce((a, b) => a + b, "");
|
return xs.reduce((a, b) => a + b, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function capitalize(s: string): string {
|
export function capitalize(s: string): string {
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { concat, sum } from './array';
|
||||||
|
|
||||||
|
export type Tree<T> = {
|
||||||
|
node: T,
|
||||||
|
children: Forest<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Forest<T> = Tree<T>[];
|
||||||
|
|
||||||
|
export function createLeaf<T>(node: T): Tree<T> {
|
||||||
|
return { node, children: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTree<T>(node: T, children: Forest<T>): Tree<T> {
|
||||||
|
return { node, children };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasChildren<T>(t: Tree<T>): boolean {
|
||||||
|
return t.children.length !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preorder<T>(t: Tree<T>): T[] {
|
||||||
|
return [t.node, ...preorderF(t.children)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preorderF<T>(ts: Forest<T>): T[] {
|
||||||
|
return concat(ts.map(preorder));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countNodes<T>(t: Tree<T>): number {
|
||||||
|
return preorder(t).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countNodesF<T>(ts: Forest<T>): number {
|
||||||
|
return sum(ts.map(countNodes));
|
||||||
|
}
|
||||||
+17
-8
@@ -2,17 +2,26 @@ const push = require('web-push');
|
|||||||
import * as mongo from 'mongodb';
|
import * as mongo from 'mongodb';
|
||||||
import Subscription from './models/sw-subscription';
|
import Subscription from './models/sw-subscription';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
import fetchMeta from './misc/fetch-meta';
|
||||||
|
import { IMeta } from './models/meta';
|
||||||
|
|
||||||
if (config.sw) {
|
let meta: IMeta = null;
|
||||||
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
|
|
||||||
push.setVapidDetails(
|
setInterval(() => {
|
||||||
config.url,
|
fetchMeta().then(m => {
|
||||||
config.sw.public_key,
|
meta = m;
|
||||||
config.sw.private_key);
|
|
||||||
}
|
if (meta.enableServiceWorker) {
|
||||||
|
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
|
||||||
|
push.setVapidDetails(config.url,
|
||||||
|
meta.swPublicKey,
|
||||||
|
meta.swPrivateKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
export default async function(userId: mongo.ObjectID | string, type: string, body?: any) {
|
export default async function(userId: mongo.ObjectID | string, type: string, body?: any) {
|
||||||
if (!config.sw) return;
|
if (!meta.enableServiceWorker) return;
|
||||||
|
|
||||||
if (typeof userId === 'string') {
|
if (typeof userId === 'string') {
|
||||||
userId = new mongo.ObjectID(userId);
|
userId = new mongo.ObjectID(userId);
|
||||||
|
|||||||
@@ -181,6 +181,20 @@ export async function extractEmojis(tags: ITag[], host_: string) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
|
if ((tag.updated != null && exists.updatedAt == null)
|
||||||
|
|| (tag.id != null && exists.uri == null)
|
||||||
|
|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)) {
|
||||||
|
return await Emoji.findOneAndUpdate({
|
||||||
|
host,
|
||||||
|
name,
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
uri: tag.id,
|
||||||
|
url: tag.icon.url,
|
||||||
|
updatedAt: new Date(tag.updated),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
return exists;
|
return exists;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,8 +203,10 @@ export async function extractEmojis(tags: ITag[], host_: string) {
|
|||||||
return await Emoji.insert({
|
return await Emoji.insert({
|
||||||
host,
|
host,
|
||||||
name,
|
name,
|
||||||
|
uri: tag.id,
|
||||||
url: tag.icon.url,
|
url: tag.icon.url,
|
||||||
aliases: [],
|
updatedAt: tag.updated ? new Date(tag.updated) : undefined,
|
||||||
|
aliases: []
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import Instance from '../../../models/instance';
|
|||||||
import getDriveFileUrl from '../../../misc/get-drive-file-url';
|
import getDriveFileUrl from '../../../misc/get-drive-file-url';
|
||||||
import { IEmoji } from '../../../models/emoji';
|
import { IEmoji } from '../../../models/emoji';
|
||||||
import { ITag } from './tag';
|
import { ITag } from './tag';
|
||||||
|
import Following from '../../../models/following';
|
||||||
|
|
||||||
const log = debug('misskey:activitypub');
|
const log = debug('misskey:activitypub');
|
||||||
|
|
||||||
@@ -164,7 +165,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
|
|||||||
publicKeyPem: person.publicKey.publicKeyPem
|
publicKeyPem: person.publicKey.publicKeyPem
|
||||||
},
|
},
|
||||||
inbox: person.inbox,
|
inbox: person.inbox,
|
||||||
sharedInbox: person.sharedInbox,
|
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||||
featured: person.featured,
|
featured: person.featured,
|
||||||
endpoints: person.endpoints,
|
endpoints: person.endpoints,
|
||||||
uri: person.id,
|
uri: person.id,
|
||||||
@@ -340,7 +341,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
|
|||||||
$set: {
|
$set: {
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
inbox: person.inbox,
|
inbox: person.inbox,
|
||||||
sharedInbox: person.sharedInbox,
|
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||||
featured: person.featured,
|
featured: person.featured,
|
||||||
avatarId: avatar ? avatar._id : null,
|
avatarId: avatar ? avatar._id : null,
|
||||||
bannerId: banner ? banner._id : null,
|
bannerId: banner ? banner._id : null,
|
||||||
@@ -368,6 +369,15 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
|
||||||
|
await Following.update({
|
||||||
|
followerId: exist._id
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
'_follower.sharedInbox': person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await updateFeatured(exist._id).catch(err => console.log(err));
|
await updateFeatured(exist._id).catch(err => console.log(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +441,7 @@ export async function updateFeatured(userId: mongo.ObjectID) {
|
|||||||
|
|
||||||
await User.update({ _id: user._id }, {
|
await User.update({ _id: user._id }, {
|
||||||
$set: {
|
$set: {
|
||||||
pinnedNoteIds: featuredNotes.map(note => note._id)
|
pinnedNoteIds: featuredNotes.filter(note => note != null).map(note => note._id)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IIcon } from "./icon";
|
import { IIcon } from './icon';
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* tag (ActivityPub)
|
* tag (ActivityPub)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
import { ILocalUser, IRemoteUser } from "../../../models/user";
|
import { ILocalUser, IRemoteUser } from '../../../models/user';
|
||||||
|
|
||||||
export default (blocker?: ILocalUser, blockee?: IRemoteUser) => ({
|
export default (blocker?: ILocalUser, blockee?: IRemoteUser) => ({
|
||||||
type: 'Block',
|
type: 'Block',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
import { ILocalUser } from "../../../models/user";
|
import { ILocalUser } from '../../../models/user';
|
||||||
|
|
||||||
export default (object: any, user: ILocalUser) => ({
|
export default (object: any, user: ILocalUser) => ({
|
||||||
type: 'Delete',
|
type: 'Delete',
|
||||||
|
|||||||
@@ -102,10 +102,9 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
|
|||||||
|
|
||||||
let apText = text;
|
let apText = text;
|
||||||
|
|
||||||
if (note.renoteId != null) {
|
if (quote) {
|
||||||
if (apText == null) apText = '';
|
if (apText == null) apText = '';
|
||||||
const url = `${config.url}/notes/${note.renoteId}`;
|
apText += `\n\nRE: ${quote}`;
|
||||||
apText += `\n\nRE: ${url}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export default async (user: ILocalUser) => {
|
|||||||
following: `${id}/following`,
|
following: `${id}/following`,
|
||||||
featured: `${id}/collections/featured`,
|
featured: `${id}/collections/featured`,
|
||||||
sharedInbox: `${config.url}/inbox`,
|
sharedInbox: `${config.url}/inbox`,
|
||||||
|
endpoints: { sharedInbox: `${config.url}/inbox` },
|
||||||
url: `${config.url}/@${user.username}`,
|
url: `${config.url}/@${user.username}`,
|
||||||
preferredUsername: user.username,
|
preferredUsername: user.username,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
import { ILocalUser, IUser } from "../../../models/user";
|
import { ILocalUser, IUser } from '../../../models/user';
|
||||||
|
|
||||||
export default (object: any, user: ILocalUser | IUser) => ({
|
export default (object: any, user: ILocalUser | IUser) => ({
|
||||||
type: 'Undo',
|
type: 'Undo',
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export interface IPerson extends IObject {
|
|||||||
following: any;
|
following: any;
|
||||||
featured?: any;
|
featured?: any;
|
||||||
outbox: any;
|
outbox: any;
|
||||||
endpoints: string[];
|
endpoints: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isCollection = (object: IObject): object is ICollection =>
|
export const isCollection = (object: IObject): object is ICollection =>
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import { createHttpJob } from '../queue';
|
|||||||
import pack from '../remote/activitypub/renderer';
|
import pack from '../remote/activitypub/renderer';
|
||||||
import Note from '../models/note';
|
import Note from '../models/note';
|
||||||
import User, { isLocalUser, ILocalUser, IUser } from '../models/user';
|
import User, { isLocalUser, ILocalUser, IUser } from '../models/user';
|
||||||
|
import Emoji from '../models/emoji';
|
||||||
import renderNote from '../remote/activitypub/renderer/note';
|
import renderNote from '../remote/activitypub/renderer/note';
|
||||||
import renderKey from '../remote/activitypub/renderer/key';
|
import renderKey from '../remote/activitypub/renderer/key';
|
||||||
import renderPerson from '../remote/activitypub/renderer/person';
|
import renderPerson from '../remote/activitypub/renderer/person';
|
||||||
|
import renderEmoji from '../remote/activitypub/renderer/emoji';
|
||||||
import Outbox, { packActivity } from './activitypub/outbox';
|
import Outbox, { packActivity } from './activitypub/outbox';
|
||||||
import Followers from './activitypub/followers';
|
import Followers from './activitypub/followers';
|
||||||
import Following from './activitypub/following';
|
import Following from './activitypub/following';
|
||||||
@@ -188,4 +190,21 @@ router.get('/@:user', async (ctx, next) => {
|
|||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
// emoji
|
||||||
|
router.get('/emojis/:emoji', async ctx => {
|
||||||
|
const emoji = await Emoji.findOne({
|
||||||
|
host: null,
|
||||||
|
name: ctx.params.emoji
|
||||||
|
});
|
||||||
|
|
||||||
|
if (emoji === null) {
|
||||||
|
ctx.status = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = pack(await renderEmoji(emoji));
|
||||||
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
|
setResponseType(ctx);
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
_sort = {
|
_sort = {
|
||||||
length: -1
|
length: -1
|
||||||
};
|
};
|
||||||
} else if (ps.sort == '+size') {
|
} else if (ps.sort == '-size') {
|
||||||
_sort = {
|
_sort = {
|
||||||
length: -1
|
length: 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -285,6 +285,27 @@ export const meta = {
|
|||||||
'ja-JP': 'SMTPサーバのパスワード'
|
'ja-JP': 'SMTPサーバのパスワード'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
enableServiceWorker: {
|
||||||
|
validator: $.bool.optional,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': 'ServiceWorkerを有効にするか否か'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
swPublicKey: {
|
||||||
|
validator: $.str.optional.nullable,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': 'ServiceWorkerのVAPIDキーペアの公開鍵'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
swPrivateKey: {
|
||||||
|
validator: $.str.optional.nullable,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': 'ServiceWorkerのVAPIDキーペアの秘密鍵'
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -447,6 +468,18 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
|
|||||||
set.errorImageUrl = ps.errorImageUrl;
|
set.errorImageUrl = ps.errorImageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.enableServiceWorker !== undefined) {
|
||||||
|
set.enableServiceWorker = ps.enableServiceWorker;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.swPublicKey !== undefined) {
|
||||||
|
set.swPublicKey = ps.swPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.swPrivateKey !== undefined) {
|
||||||
|
set.swPrivateKey = ps.swPrivateKey;
|
||||||
|
}
|
||||||
|
|
||||||
await Meta.update({}, {
|
await Meta.update({}, {
|
||||||
$set: set
|
$set: set
|
||||||
}, { upsert: true });
|
}, { upsert: true });
|
||||||
|
|||||||
@@ -25,11 +25,10 @@ export const meta = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default define(meta, (ps) => new Promise(async (res, rej) => {
|
export default define(meta, (ps) => new Promise((res, rej) => {
|
||||||
const object = await fetchAny(ps.uri);
|
fetchAny(ps.uri)
|
||||||
if (object == null) return rej('object not found');
|
.then(object => object != null ? res(object) : rej('object not found'))
|
||||||
|
.catch(e => rej(e));
|
||||||
res(object);
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/***
|
/***
|
||||||
|
|||||||
@@ -57,14 +57,17 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
|||||||
// Fetch file
|
// Fetch file
|
||||||
const file = await DriveFile
|
const file = await DriveFile
|
||||||
.findOne({
|
.findOne({
|
||||||
_id: ps.fileId,
|
_id: ps.fileId
|
||||||
'metadata.userId': user._id
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (file === null) {
|
if (file === null) {
|
||||||
return rej('file-not-found');
|
return rej('file-not-found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) {
|
||||||
|
return rej('access denied');
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.name) file.filename = ps.name;
|
if (ps.name) file.filename = ps.name;
|
||||||
|
|
||||||
if (ps.isSensitive !== undefined) file.metadata.isSensitive = ps.isSensitive;
|
if (ps.isSensitive !== undefined) file.metadata.isSensitive = ps.isSensitive;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { publishToFollowers } from '../../../../services/i/update';
|
|||||||
import define from '../../define';
|
import define from '../../define';
|
||||||
import getDriveFileUrl from '../../../../misc/get-drive-file-url';
|
import getDriveFileUrl from '../../../../misc/get-drive-file-url';
|
||||||
import parse from '../../../../mfm/parse';
|
import parse from '../../../../mfm/parse';
|
||||||
import { extractEmojis } from '../../../../services/note/create';
|
import extractEmojis from '../../../../misc/extract-emojis';
|
||||||
const langmap = require('langmap');
|
const langmap = require('langmap');
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||||
enableRecaptcha: instance.enableRecaptcha,
|
enableRecaptcha: instance.enableRecaptcha,
|
||||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||||
swPublickey: config.sw ? config.sw.public_key : null,
|
swPublickey: instance.swPublicKey,
|
||||||
bannerUrl: instance.bannerUrl,
|
bannerUrl: instance.bannerUrl,
|
||||||
errorImageUrl: instance.errorImageUrl,
|
errorImageUrl: instance.errorImageUrl,
|
||||||
maxNoteTextLength: instance.maxNoteTextLength,
|
maxNoteTextLength: instance.maxNoteTextLength,
|
||||||
@@ -85,7 +85,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
twitter: instance.enableTwitterIntegration,
|
twitter: instance.enableTwitterIntegration,
|
||||||
github: instance.enableGithubIntegration,
|
github: instance.enableGithubIntegration,
|
||||||
discord: instance.enableDiscordIntegration,
|
discord: instance.enableDiscordIntegration,
|
||||||
serviceWorker: config.sw ? true : false,
|
serviceWorker: instance.enableServiceWorker,
|
||||||
userRecommendation: {
|
userRecommendation: {
|
||||||
external: instance.enableExternalUserRecommendation,
|
external: instance.enableExternalUserRecommendation,
|
||||||
engine: instance.externalUserRecommendationEngine,
|
engine: instance.externalUserRecommendationEngine,
|
||||||
@@ -114,6 +114,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
response.smtpPort = instance.smtpPort;
|
response.smtpPort = instance.smtpPort;
|
||||||
response.smtpUser = instance.smtpUser;
|
response.smtpUser = instance.smtpUser;
|
||||||
response.smtpPass = instance.smtpPass;
|
response.smtpPass = instance.smtpPass;
|
||||||
|
response.swPrivateKey = instance.swPrivateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
res(response);
|
res(response);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const meta = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
visibleUserIds: {
|
visibleUserIds: {
|
||||||
validator: $.arr($.type(ID)).optional.unique().min(1),
|
validator: $.arr($.type(ID)).optional.unique().min(0),
|
||||||
transform: transformMany,
|
transform: transformMany,
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '(投稿の公開範囲が specified の場合)投稿を閲覧できるユーザー'
|
'ja-JP': '(投稿の公開範囲が specified の場合)投稿を閲覧できるユーザー'
|
||||||
@@ -82,6 +82,30 @@ export const meta = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
noExtractMentions: {
|
||||||
|
validator: $.bool.optional,
|
||||||
|
default: false,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '本文からメンションを展開しないか否か。'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
noExtractHashtags: {
|
||||||
|
validator: $.bool.optional,
|
||||||
|
default: false,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '本文からハッシュタグを展開しないか否か。'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
noExtractEmojis: {
|
||||||
|
validator: $.bool.optional,
|
||||||
|
default: false,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '本文からカスタム絵文字を展開しないか否か。'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
geo: {
|
geo: {
|
||||||
validator: $.obj({
|
validator: $.obj({
|
||||||
coordinates: $.arr().length(2)
|
coordinates: $.arr().length(2)
|
||||||
@@ -237,6 +261,9 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
|
|||||||
localOnly: ps.localOnly,
|
localOnly: ps.localOnly,
|
||||||
visibility: ps.visibility,
|
visibility: ps.visibility,
|
||||||
visibleUsers,
|
visibleUsers,
|
||||||
|
apMentions: ps.noExtractMentions ? [] : undefined,
|
||||||
|
apHashtags: ps.noExtractHashtags ? [] : undefined,
|
||||||
|
apEmojis: ps.noExtractEmojis ? [] : undefined,
|
||||||
geo: ps.geo
|
geo: ps.geo
|
||||||
})
|
})
|
||||||
.then(note => pack(note, user))
|
.then(note => pack(note, user))
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
|||||||
|
|
||||||
const notes = await Note
|
const notes = await Note
|
||||||
.find({
|
.find({
|
||||||
|
'_user.host': null,
|
||||||
_id: {
|
_id: {
|
||||||
$nin: nin
|
$nin: nin
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import Subscription from '../../../../models/sw-subscription';
|
import Subscription from '../../../../models/sw-subscription';
|
||||||
import config from '../../../../config';
|
|
||||||
import define from '../../define';
|
import define from '../../define';
|
||||||
|
import fetchMeta from '../../../../misc/fetch-meta';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
@@ -31,10 +31,12 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
|||||||
deletedAt: { $exists: false }
|
deletedAt: { $exists: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const instance = await fetchMeta();
|
||||||
|
|
||||||
if (exist != null) {
|
if (exist != null) {
|
||||||
return res({
|
return res({
|
||||||
state: 'already-subscribed',
|
state: 'already-subscribed',
|
||||||
key: config.sw.public_key
|
key: instance.swPublicKey
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +49,6 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
|||||||
|
|
||||||
res({
|
res({
|
||||||
state: 'subscribed',
|
state: 'subscribed',
|
||||||
key: config.sw.public_key
|
key: instance.swPublicKey
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
|
||||||
|
import UserList from '../../../../../models/user-list';
|
||||||
|
import User, { pack as packUser } from '../../../../../models/user';
|
||||||
|
import { publishUserListStream } from '../../../../../stream';
|
||||||
|
import define from '../../../define';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '指定したユーザーリストから指定したユーザーを削除します。',
|
||||||
|
'en-US': 'Remove a user to a user list.'
|
||||||
|
},
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'account-write',
|
||||||
|
|
||||||
|
params: {
|
||||||
|
listId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
transform: transform,
|
||||||
|
},
|
||||||
|
|
||||||
|
userId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
transform: transform,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '対象のユーザーのID',
|
||||||
|
'en-US': 'Target user ID'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
||||||
|
// Fetch the list
|
||||||
|
const userList = await UserList.findOne({
|
||||||
|
_id: ps.listId,
|
||||||
|
userId: me._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userList == null) {
|
||||||
|
return rej('list not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the user
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: ps.userId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return rej('user not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the user
|
||||||
|
await UserList.update({ _id: userList._id }, {
|
||||||
|
$pull: {
|
||||||
|
userIds: user._id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res();
|
||||||
|
|
||||||
|
publishUserListStream(userList._id, 'userRemoved', await packUser(user));
|
||||||
|
}));
|
||||||
@@ -53,7 +53,7 @@ export const meta = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
||||||
const isUsername = validateUsername(ps.query.replace('@', ''), true);
|
const isUsername = validateUsername(ps.query.replace('@', ''), !ps.localOnly);
|
||||||
|
|
||||||
let users: IUser[] = [];
|
let users: IUser[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ export default class extends Channel {
|
|||||||
|
|
||||||
// Subscribe stream
|
// Subscribe stream
|
||||||
this.subscriber.on('hashtag', async note => {
|
this.subscriber.on('hashtag', async note => {
|
||||||
const matched = q.some(tags => tags.every(tag => note.tags.map((t: string) => t.toLowerCase()).includes(tag.toLowerCase())));
|
const noteTags = note.tags.map((t: string) => t.toLowerCase());
|
||||||
|
const matched = q.some(tags => tags.every(tag => noteTags.includes(tag.toLowerCase())));
|
||||||
if (!matched) return;
|
if (!matched) return;
|
||||||
|
|
||||||
// Renoteなら再pack
|
// Renoteなら再pack
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Feed } from 'feed';
|
||||||
|
import config from '../../config';
|
||||||
|
import Note from '../../models/note';
|
||||||
|
import { IUser } from '../../models/user';
|
||||||
|
import { getOriginalUrl } from '../../misc/get-drive-file-url';
|
||||||
|
|
||||||
|
export default async function(user: IUser) {
|
||||||
|
const author: Author = {
|
||||||
|
link: `${config.url}/@${user.username}`,
|
||||||
|
name: user.name || user.username
|
||||||
|
};
|
||||||
|
|
||||||
|
const notes = await Note.find({
|
||||||
|
userId: user._id,
|
||||||
|
renoteId: null,
|
||||||
|
$or: [
|
||||||
|
{ visibility: 'public' },
|
||||||
|
{ visibility: 'home' }
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
sort: { createdAt: -1 },
|
||||||
|
limit: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
const feed = new Feed({
|
||||||
|
id: author.link,
|
||||||
|
title: `${author.name} (@${user.username}@${config.host})`,
|
||||||
|
updated: notes[0].createdAt,
|
||||||
|
generator: 'Misskey',
|
||||||
|
description: `${user.notesCount} Notes, ${user.followingCount} Following, ${user.followersCount} Followers${user.description ? ` · ${user.description}` : ''}`,
|
||||||
|
link: author.link,
|
||||||
|
image: user.avatarUrl,
|
||||||
|
feedLinks: {
|
||||||
|
json: `${author.link}.json`,
|
||||||
|
atom: `${author.link}.atom`,
|
||||||
|
},
|
||||||
|
author
|
||||||
|
} as FeedOptions);
|
||||||
|
|
||||||
|
for (const note of notes) {
|
||||||
|
const file = note._files && note._files.find(file => file.contentType.startsWith('image/'));
|
||||||
|
|
||||||
|
feed.addItem({
|
||||||
|
title: `New note by ${author.name}`,
|
||||||
|
link: `${config.url}/notes/${note._id}`,
|
||||||
|
date: note.createdAt,
|
||||||
|
description: note.cw,
|
||||||
|
content: note.text,
|
||||||
|
image: file && getOriginalUrl(file)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return feed;
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import * as favicon from 'koa-favicon';
|
|||||||
import * as views from 'koa-views';
|
import * as views from 'koa-views';
|
||||||
|
|
||||||
import docs from './docs';
|
import docs from './docs';
|
||||||
|
import packFeed from './feed';
|
||||||
import User from '../../models/user';
|
import User from '../../models/user';
|
||||||
import parseAcct from '../../misc/acct/parse';
|
import parseAcct from '../../misc/acct/parse';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
@@ -82,6 +83,52 @@ router.use('/docs', docs.routes());
|
|||||||
// URL preview endpoint
|
// URL preview endpoint
|
||||||
router.get('/url', require('./url-preview'));
|
router.get('/url', require('./url-preview'));
|
||||||
|
|
||||||
|
const getFeed = async (acct: string) => {
|
||||||
|
const { username, host } = parseAcct(acct);
|
||||||
|
const user = await User.findOne({
|
||||||
|
usernameLower: username.toLowerCase(),
|
||||||
|
host
|
||||||
|
});
|
||||||
|
|
||||||
|
return user && await packFeed(user);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Atom
|
||||||
|
router.get('/@:user.atom', async ctx => {
|
||||||
|
const feed = await getFeed(ctx.params.user);
|
||||||
|
|
||||||
|
if (feed) {
|
||||||
|
ctx.set('Content-Type', 'application/atom+xml; charset=utf-8');
|
||||||
|
ctx.body = feed.atom1();
|
||||||
|
} else {
|
||||||
|
ctx.status = 404;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// RSS
|
||||||
|
router.get('/@:user.rss', async ctx => {
|
||||||
|
const feed = await getFeed(ctx.params.user);
|
||||||
|
|
||||||
|
if (feed) {
|
||||||
|
ctx.set('Content-Type', 'application/rss+xml; charset=utf-8');
|
||||||
|
ctx.body = feed.rss2();
|
||||||
|
} else {
|
||||||
|
ctx.status = 404;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
router.get('/@:user.json', async ctx => {
|
||||||
|
const feed = await getFeed(ctx.params.user);
|
||||||
|
|
||||||
|
if (feed) {
|
||||||
|
ctx.set('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
ctx.body = feed.json1();
|
||||||
|
} else {
|
||||||
|
ctx.status = 404;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//#region for crawlers
|
//#region for crawlers
|
||||||
// User
|
// User
|
||||||
router.get('/@:user', async (ctx, next) => {
|
router.get('/@:user', async (ctx, next) => {
|
||||||
|
|||||||
+19
-56
@@ -24,11 +24,13 @@ import isQuote from '../../misc/is-quote';
|
|||||||
import notesChart from '../../chart/notes';
|
import notesChart from '../../chart/notes';
|
||||||
import perUserNotesChart from '../../chart/per-user-notes';
|
import perUserNotesChart from '../../chart/per-user-notes';
|
||||||
|
|
||||||
import { erase, unique } from '../../prelude/array';
|
import { erase } from '../../prelude/array';
|
||||||
import insertNoteUnread from './unread';
|
import insertNoteUnread from './unread';
|
||||||
import registerInstance from '../register-instance';
|
import registerInstance from '../register-instance';
|
||||||
import Instance from '../../models/instance';
|
import Instance from '../../models/instance';
|
||||||
import { Node } from '../../mfm/parser';
|
import extractMentions from '../../misc/extract-mentions';
|
||||||
|
import extractEmojis from '../../misc/extract-emojis';
|
||||||
|
import extractHashtags from '../../misc/extract-hashtags';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
@@ -162,14 +164,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
|
|||||||
|
|
||||||
tags = data.apHashtags || extractHashtags(combinedTokens);
|
tags = data.apHashtags || extractHashtags(combinedTokens);
|
||||||
|
|
||||||
// MongoDBのインデックス対象は128文字以上にできない
|
|
||||||
tags = tags.filter(tag => tag.length <= 100);
|
|
||||||
|
|
||||||
emojis = data.apEmojis || extractEmojis(combinedTokens);
|
emojis = data.apEmojis || extractEmojis(combinedTokens);
|
||||||
|
|
||||||
mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens);
|
mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MongoDBのインデックス対象は128文字以上にできない
|
||||||
|
tags = tags.filter(tag => tag.length <= 100);
|
||||||
|
|
||||||
if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) {
|
if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) {
|
||||||
mentionedUsers.push(await User.findOne({ _id: data.reply.userId }));
|
mentionedUsers.push(await User.findOne({ _id: data.reply.userId }));
|
||||||
}
|
}
|
||||||
@@ -180,6 +182,17 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
|
|||||||
mentionedUsers.push(u);
|
mentionedUsers.push(u);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const u of mentionedUsers) {
|
||||||
|
if (!data.visibleUsers.some(x => x._id.equals(u._id))) {
|
||||||
|
data.visibleUsers.push(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ダイレクト投稿でユーザーが指定されていなかったらreject
|
||||||
|
if (data.visibleUsers.length === 0) {
|
||||||
|
return rej('Target user is not specified');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const note = await insertNote(user, data, tags, emojis, mentionedUsers);
|
const note = await insertNote(user, data, tags, emojis, mentionedUsers);
|
||||||
@@ -465,44 +478,6 @@ async function insertNote(user: IUser, data: Option, tags: string[], emojis: str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractHashtags(tokens: ReturnType<typeof parse>): string[] {
|
|
||||||
const hashtags: string[] = [];
|
|
||||||
|
|
||||||
const extract = (tokens: Node[]) => {
|
|
||||||
for (const x of tokens.filter(x => x.name === 'hashtag')) {
|
|
||||||
hashtags.push(x.props.hashtag);
|
|
||||||
}
|
|
||||||
for (const x of tokens.filter(x => x.children)) {
|
|
||||||
extract(x.children);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract hashtags
|
|
||||||
extract(tokens);
|
|
||||||
|
|
||||||
return unique(hashtags);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractEmojis(tokens: ReturnType<typeof parse>): string[] {
|
|
||||||
const emojis: string[] = [];
|
|
||||||
|
|
||||||
const extract = (tokens: Node[]) => {
|
|
||||||
for (const x of tokens.filter(x => x.name === 'emoji')) {
|
|
||||||
if (x.props.name && x.props.name.length <= 100) {
|
|
||||||
emojis.push(x.props.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const x of tokens.filter(x => x.children)) {
|
|
||||||
extract(x.children);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract emojis
|
|
||||||
extract(tokens);
|
|
||||||
|
|
||||||
return unique(emojis);
|
|
||||||
}
|
|
||||||
|
|
||||||
function index(note: INote) {
|
function index(note: INote) {
|
||||||
if (note.text == null || config.elasticsearch == null) return;
|
if (note.text == null || config.elasticsearch == null) return;
|
||||||
|
|
||||||
@@ -665,19 +640,7 @@ function incNotesCount(user: IUser) {
|
|||||||
async function extractMentionedUsers(user: IUser, tokens: ReturnType<typeof parse>): Promise<IUser[]> {
|
async function extractMentionedUsers(user: IUser, tokens: ReturnType<typeof parse>): Promise<IUser[]> {
|
||||||
if (tokens == null) return [];
|
if (tokens == null) return [];
|
||||||
|
|
||||||
const mentions: any[] = [];
|
const mentions = extractMentions(tokens);
|
||||||
|
|
||||||
const extract = (tokens: Node[]) => {
|
|
||||||
for (const x of tokens.filter(x => x.name === 'mention')) {
|
|
||||||
mentions.push(x.props);
|
|
||||||
}
|
|
||||||
for (const x of tokens.filter(x => x.children)) {
|
|
||||||
extract(x.children);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract hashtags
|
|
||||||
extract(tokens);
|
|
||||||
|
|
||||||
let mentionedUsers =
|
let mentionedUsers =
|
||||||
erase(null, await Promise.all(mentions.map(async m => {
|
erase(null, await Promise.all(mentions.map(async m => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as debug from 'debug';
|
import * as debug from 'debug';
|
||||||
import Emoji from "../models/emoji";
|
import Emoji from '../models/emoji';
|
||||||
|
|
||||||
debug.enable('*');
|
debug.enable('*');
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import parseAcct from "../misc/acct/parse";
|
import parseAcct from '../misc/acct/parse';
|
||||||
import resolveUser from '../remote/resolve-user';
|
import resolveUser from '../remote/resolve-user';
|
||||||
import * as debug from 'debug';
|
import * as debug from 'debug';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import * as assert from 'assert';
|
||||||
|
|
||||||
|
import extractMentions from '../src/misc/extract-mentions';
|
||||||
|
import parse from '../src/mfm/parse';
|
||||||
|
|
||||||
|
describe('Extract mentions', () => {
|
||||||
|
it('simple', () => {
|
||||||
|
const ast = parse('@foo @bar @baz');
|
||||||
|
const mentions = extractMentions(ast);
|
||||||
|
assert.deepStrictEqual(mentions, [{
|
||||||
|
username: 'foo',
|
||||||
|
acct: '@foo',
|
||||||
|
canonical: '@foo',
|
||||||
|
host: null
|
||||||
|
}, {
|
||||||
|
username: 'bar',
|
||||||
|
acct: '@bar',
|
||||||
|
canonical: '@bar',
|
||||||
|
host: null
|
||||||
|
}, {
|
||||||
|
username: 'baz',
|
||||||
|
acct: '@baz',
|
||||||
|
canonical: '@baz',
|
||||||
|
host: null
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nested', () => {
|
||||||
|
const ast = parse('@foo **@bar** @baz');
|
||||||
|
const mentions = extractMentions(ast);
|
||||||
|
assert.deepStrictEqual(mentions, [{
|
||||||
|
username: 'foo',
|
||||||
|
acct: '@foo',
|
||||||
|
canonical: '@foo',
|
||||||
|
host: null
|
||||||
|
}, {
|
||||||
|
username: 'bar',
|
||||||
|
acct: '@bar',
|
||||||
|
canonical: '@bar',
|
||||||
|
host: null
|
||||||
|
}, {
|
||||||
|
username: 'baz',
|
||||||
|
acct: '@baz',
|
||||||
|
canonical: '@baz',
|
||||||
|
host: null
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
+439
-304
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user