Compare commits
	
		
			91 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 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 | ||
|   | 48dc56e834 | ||
|   | 2c33bd6e31 | ||
|   | b6524616bc | ||
|   | 7e2b70f912 | ||
|   | 4f071a66b6 | ||
|   | 39f2303429 | ||
|   | cacf072027 | ||
|   | 6ab1fdfe1a | ||
|   | 6e5c93f926 | ||
|   | 1670737075 | ||
|   | fee235c4e4 | ||
|   | 7a39d489f2 | ||
|   | 7c634218d1 | ||
|   | 2704c5be73 | ||
|   | 489b51ba9f | ||
|   | 21807c29f1 | ||
|   | 3bc62fe3eb | ||
|   | ba0e3c4a5f | ||
|   | 9ec1fb5e37 | ||
|   | d708409462 | ||
|   | 07d05d4f86 | ||
|   | bbdb2ebb40 | ||
|   | f7908ba098 | ||
|   | f2fda3075e | ||
|   | 1338a68979 | ||
|   | e7da505fb3 | ||
|   | 5a9228372f | ||
|   | c4a6ba9097 | 
| @@ -108,13 +108,5 @@ autoAdmin: true | ||||
| #  port: 9200 | ||||
| #  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 | ||||
| #clusterLimit: 1 | ||||
|   | ||||
							
								
								
									
										45
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,6 +1,51 @@ | ||||
| ChangeLog | ||||
| ========= | ||||
|  | ||||
| 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 | ||||
| ------- | ||||
| * UIの動きを減らすオプションが一部のアニメーションに適用されなかったのを修正 | ||||
|  | ||||
| 10.64.1 | ||||
| ------- | ||||
| * レートリミットの調整 | ||||
| * アニメーションの調整 | ||||
|  | ||||
| 10.64.0 | ||||
| ------- | ||||
| * いくつかのアニメーションを追加 | ||||
|   | ||||
| @@ -25,3 +25,16 @@ Misskey uses [vue-i18n](https://github.com/kazupon/vue-i18n). | ||||
| ## Continuous integration | ||||
| Misskey uses CircleCI for automated test. | ||||
| 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_. | ||||
|   | ||||
							
								
								
									
										13
									
								
								PULL_REQUEST_TEMPLATE.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								PULL_REQUEST_TEMPLATE.md
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
|   - | ||||
|   --> | ||||
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								README.md
									
									
									
									
									
								
							| @@ -3,9 +3,9 @@ | ||||
| [](https://misskey.xyz/) | ||||
| ================================================================ | ||||
|  | ||||
| [](https://circleci.com/gh/syuilo/misskey) | ||||
| [![][dependencies-badge]][dependencies-link] | ||||
| [](http://makeapullrequest.com) | ||||
| [](https://circleci.com/gh/syuilo/misskey) | ||||
| [](https://david-dm.org/syuilo/misskey) | ||||
| [](http://makeapullrequest.com) | ||||
|  | ||||
| **Sophisticated microblogging platform, evolving forever.** | ||||
|  | ||||
| @@ -77,7 +77,7 @@ Please see [Contribution guide](./CONTRIBUTING.md). | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/13376668/71f3cf87ec6c4393a44b1b9df5ee3d12/1?token-time=2145916800&token-hash=7pSmWqgMfMSJHVIEcNsuuQoKeU3TRluew5p0EGTzWA4%3D" alt="Arctic"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12731202/0995c46cdcb54153ab5f073f5869b70a/1?token-time=2145916800&token-hash=Yd60FK_SWfQO56SeiJpy1tDHOnCV4xdEywQe8gn5_Wo%3D" alt="negao"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/2?token-time=2145916800&token-hash=mgPdX9TqZxEg4TTPuc477dxhIgYk9246qafjWZEqZ7g%3D" alt="Melilot"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12999811/5f349fafcce44dd1824a8b1ebbec4564/2?token-time=2145916800&token-hash=rwZ8qvbm_kpA4ib3kc07tVKupXeySpY5ATQFGxfL9v0%3D" alt="Xeltica"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12999811/5f349fafcce44dd1824a8b1ebbec4564/3?token-time=2145916800&token-hash=ybYtxfpte1b-rGg6Zecpys2ZdZDtwR_UNJHQjt-3eoU%3D" alt="Xeltica"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/3384329/8b713330cb27404ea6e9fac50ff96efe/1?token-time=2145916800&token-hash=0eu4-m1gTWA9PhptVZt6rdKcusqcD7RB87rJT23VVFI%3D" alt="べすれい"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=GgJ_NmUB6_nnRNLVGUWjV-WX91On7BOu59LKncYV9fE%3D" alt="gutfuckllc"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/11357794/923ce94cd8c44ba788ee931907881839/1?token-time=2145916800&token-hash=I8lJVM8LeW6TSo5W6uIIRZ42cw83zp1wK_FsbzY0mcQ%3D" alt="mydarkstar"></td> | ||||
| @@ -86,7 +86,7 @@ Please see [Contribution guide](./CONTRIBUTING.md). | ||||
| <td><a href="https://www.patreon.com/user?u=13376668">Arctic</a></td> | ||||
| <td><a href="https://www.patreon.com/negao">negao</a></td> | ||||
| <td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td> | ||||
| <td><a href="https://www.patreon.com/AxellaMC">Xeltica</a></td> | ||||
| <td><a href="https://www.patreon.com/Xeltica">Xeltica</a></td> | ||||
| <td><a href="https://www.patreon.com/user?u=3384329">べすれい</a></td> | ||||
| <td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td> | ||||
| <td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td> | ||||
| @@ -118,7 +118,7 @@ Please see [Contribution guide](./CONTRIBUTING.md). | ||||
| <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td> | ||||
| </tr></table> | ||||
|  | ||||
| **Last updated:** Thu, 06 Dec 2018 14:22:05 UTC | ||||
| **Last updated:** Sun, 16 Dec 2018 18:32:06 UTC | ||||
| <!-- PATREON_END --> | ||||
|  | ||||
| :four_leaf_clover: Copyright | ||||
| @@ -130,9 +130,7 @@ Misskey is an open-source software licensed under the [GNU AGPLv3](LICENSE). | ||||
| [![][agpl-3.0-badge]][AGPL-3.0] | ||||
|  | ||||
| [agpl-3.0]:           https://www.gnu.org/licenses/agpl-3.0.en.html | ||||
| [agpl-3.0-badge]:     https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square | ||||
| [dependencies-link]:  https://david-dm.org/syuilo/misskey | ||||
| [dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square | ||||
| [agpl-3.0-badge]:     https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=for-the-badge | ||||
|  | ||||
| [backer-url]: #backers | ||||
| [backer-badge]: https://opencollective.com/misskey/backers/badge.svg | ||||
|   | ||||
							
								
								
									
										70
									
								
								docs/examples/misskey.nginx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								docs/examples/misskey.nginx
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| 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 | ||||
| ---------------------------------------------------------------- | ||||
| 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) | ||||
| 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 | ||||
| ---------------------------------------------------------------- | ||||
| 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)を確認 | ||||
| 5. `npm install` Misskeyの依存パッケージをインストール | ||||
|  | ||||
| *(オプション)* VAPIDキーペアの生成 | ||||
| ---------------------------------------------------------------- | ||||
| ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります: | ||||
|  | ||||
| ``` shell | ||||
| npm install web-push -g | ||||
| web-push generate-vapid-keys | ||||
| ``` | ||||
|  | ||||
| *5.* 設定ファイルを作成する | ||||
| ---------------------------------------------------------------- | ||||
| 1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする。 | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "私は(プリンよりむしろ)寿司が好き" | ||||
|   show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示" | ||||
|   use-white-black-reversi-stones: "リバーシに白黒の石を使う" | ||||
|   use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける" | ||||
|   verified-user: "公式アカウント" | ||||
|   disable-animated-mfm: "投稿内の動きのあるテキストを無効にする" | ||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "Ich bevorzuge Sushi anstelle von Pudding" | ||||
|   show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示" | ||||
|   use-white-black-reversi-stones: "リバーシに白黒の石を使う" | ||||
|   use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける" | ||||
|   verified-user: "Verifizierter Benutzer" | ||||
|   disable-animated-mfm: "投稿内の動きのあるテキストを無効にする" | ||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "I prefer sushi rather than pudding" | ||||
|   show-reversi-board-labels: "Show row and column labels in Reversi" | ||||
|   use-white-black-reversi-stones: "Use white-black stone in reversi" | ||||
|   use-contrast-reversi-stones: "Make the stone color clear in reversi" | ||||
|   verified-user: "Verified account" | ||||
|   disable-animated-mfm: "Disable animated texts in a post" | ||||
|   suggest-recent-hashtags: "Suggest recently used hashtags within the post composition area" | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "Prefiero sushi a pudín" | ||||
|   show-reversi-board-labels: "Mostrar etiquetas de filas y columnas en Reversi" | ||||
|   use-white-black-reversi-stones: "リバーシに白黒の石を使う" | ||||
|   use-contrast-reversi-stones: "Hacer el color de la piedra claro en Reversi" | ||||
|   verified-user: "Cuenta verificada" | ||||
|   disable-animated-mfm: "Desactivar texto animado en una publicación" | ||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "Je préfère les sushis plutôt que le pudding" | ||||
|   show-reversi-board-labels: "Afficher les étiquettes des lignes et colonnes dans Reversi" | ||||
|   use-white-black-reversi-stones: "Jouer avec des pions noirs et blancs sur Reversi" | ||||
|   use-contrast-reversi-stones: "Icône avec contraste sur Reversi" | ||||
|   verified-user: "Compte vérifié" | ||||
|   disable-animated-mfm: "Désactiver les textes animés dans les publications" | ||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "私は(プリンよりむしろ)寿司が好き" | ||||
|   show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示" | ||||
|   use-white-black-reversi-stones: "リバーシに白黒の石を使う" | ||||
|   use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける" | ||||
|   verified-user: "公式アカウント" | ||||
|   disable-animated-mfm: "投稿内の動きのあるテキストを無効にする" | ||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||
|   | ||||
| @@ -119,7 +119,6 @@ common: | ||||
|   i-like-sushi: "私は(プリンよりむしろ)寿司が好き" | ||||
|   show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示" | ||||
|   use-white-black-reversi-stones: "リバーシに白黒の石を使う" | ||||
|   use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける" | ||||
|   verified-user: "公式アカウント" | ||||
|   disable-animated-mfm: "投稿内の動きのあるテキストを無効にする" | ||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||
| @@ -520,6 +519,14 @@ common/views/components/profile-editor.vue: | ||||
|   email-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: | ||||
|   fetching: "確認中" | ||||
|   no-broadcasts: "お知らせはありません" | ||||
| @@ -1157,6 +1164,12 @@ admin/views/instance.vue: | ||||
|   smtp-port: "SMTPポート" | ||||
|   smtp-user: "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: | ||||
|   title: "チャート" | ||||
| @@ -1198,6 +1211,8 @@ admin/views/drive.vue: | ||||
|     remote: "リモート" | ||||
|   delete: "削除" | ||||
|   deleted: "削除しました" | ||||
|   mark-as-sensitive: "閲覧注意に設定" | ||||
|   unmark-as-sensitive: "閲覧注意を解除" | ||||
|  | ||||
| admin/views/users.vue: | ||||
|   operation: "操作" | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "寿司(のほうがプリンよりむしろ)ウマい、タコ焼きはあらへんけど。" | ||||
|   show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示や!" | ||||
|   use-white-black-reversi-stones: "リバーシに白黒の石を使う" | ||||
|   use-contrast-reversi-stones: "リバーシのアイコンにコントラストをつけんで!" | ||||
|   verified-user: "アメちゃん付きアカウント" | ||||
|   disable-animated-mfm: "投稿内のちょろちょろ動いてんのを止める" | ||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "저는 (푸딩보다 차라리) 초밥이 좋아요" | ||||
|   show-reversi-board-labels: "리버시 보드의 행과 열 레이블을 표시" | ||||
|   use-white-black-reversi-stones: "리버시에 흑백 돌을 사용" | ||||
|   use-contrast-reversi-stones: "리버시 아이콘의 대비를 높이기" | ||||
|   verified-user: "공식 계정" | ||||
|   disable-animated-mfm: "글의 문자 애니메이션을 비활성화" | ||||
|   suggest-recent-hashtags: "최근 해시태그를 글 작성란에 표시" | ||||
| @@ -1057,18 +1056,18 @@ admin/views/charts.vue: | ||||
|     network-usage: "통신량" | ||||
| admin/views/drive.vue: | ||||
|   sort: | ||||
|     title: "ソート" | ||||
|     createdAtAsc: "アップロード日時が古い順" | ||||
|     createdAtDesc: "アップロード日時が新しい順" | ||||
|     sizeAsc: "サイズが小さい順" | ||||
|     sizeDesc: "サイズが大きい順" | ||||
|     title: "정렬" | ||||
|     createdAtAsc: "업로드 날짜 오랜 순" | ||||
|     createdAtDesc: "업로드 날짜 최신순" | ||||
|     sizeAsc: "크기가 작은 순" | ||||
|     sizeDesc: "크기가 큰 순" | ||||
|   origin: | ||||
|     title: "オリジン" | ||||
|     combined: "ローカル+リモート" | ||||
|     local: "ローカル" | ||||
|     remote: "リモート" | ||||
|   delete: "削除" | ||||
|   deleted: "削除しました" | ||||
|     title: "출처" | ||||
|     combined: "로컬 + 리모트" | ||||
|     local: "로컬" | ||||
|     remote: "리모트" | ||||
|   delete: "삭제" | ||||
|   deleted: "삭제하였습니다" | ||||
| admin/views/users.vue: | ||||
|   operation: "작업" | ||||
|   username-or-userid: "사용자명 혹은 사용자 ID" | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "私は(プリンよりむしろ)寿司が好き" | ||||
|   show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示" | ||||
|   use-white-black-reversi-stones: "リバーシに白黒の石を使う" | ||||
|   use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける" | ||||
|   verified-user: "公式アカウント" | ||||
|   disable-animated-mfm: "投稿内の動きのあるテキストを無効にする" | ||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "私は(プリンよりむしろ)寿司が好き" | ||||
|   show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示" | ||||
|   use-white-black-reversi-stones: "リバーシに白黒の石を使う" | ||||
|   use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける" | ||||
|   verified-user: "公式アカウント" | ||||
|   disable-animated-mfm: "投稿内の動きのあるテキストを無効にする" | ||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "Wolę sushi od puddingu" | ||||
|   show-reversi-board-labels: "Pokazuj podpisy wierszy i kolumn w Reversi" | ||||
|   use-white-black-reversi-stones: "リバーシに白黒の石を使う" | ||||
|   use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける" | ||||
|   verified-user: "Zweryfikowane konto" | ||||
|   disable-animated-mfm: "Wyłącz animowany tekst we wpisach" | ||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "Eu prefiro sushi a pudim" | ||||
|   show-reversi-board-labels: "Mostrar etiquetas de colunas e linhas no Reversi" | ||||
|   use-white-black-reversi-stones: "リバーシに白黒の石を使う" | ||||
|   use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける" | ||||
|   verified-user: "Conta verificada" | ||||
|   disable-animated-mfm: "Desativar texto animado nas publicações" | ||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "私は(プリンよりむしろ)寿司が好き" | ||||
|   show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示" | ||||
|   use-white-black-reversi-stones: "リバーシに白黒の石を使う" | ||||
|   use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける" | ||||
|   verified-user: "公式アカウント" | ||||
|   disable-animated-mfm: "Отключить анимированный текст в постах" | ||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||
|   | ||||
| @@ -111,7 +111,6 @@ common: | ||||
|   i-like-sushi: "相比于布丁来说, 我更喜欢寿司。" | ||||
|   show-reversi-board-labels: "在 Reversi 中显示行和列表签" | ||||
|   use-white-black-reversi-stones: "リバーシに白黒の石を使う" | ||||
|   use-contrast-reversi-stones: "Make the stone color clear in Reversi" | ||||
|   verified-user: "认证用户" | ||||
|   disable-animated-mfm: "在帖子中禁用动画文本" | ||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| { | ||||
| 	"name": "misskey", | ||||
| 	"author": "syuilo <i@syuilo.com>", | ||||
| 	"version": "10.64.0", | ||||
| 	"clientVersion": "2.0.12768", | ||||
| 	"version": "10.66.1", | ||||
| 	"clientVersion": "2.0.12859", | ||||
| 	"codename": "nighthike", | ||||
| 	"main": "./built/index.js", | ||||
| 	"private": true, | ||||
| @@ -114,6 +114,7 @@ | ||||
| 		"eslint": "5.8.0", | ||||
| 		"eslint-plugin-vue": "4.7.1", | ||||
| 		"eventemitter3": "3.1.0", | ||||
| 		"feed": "2.0.2", | ||||
| 		"file-loader": "2.0.0", | ||||
| 		"file-type": "10.6.0", | ||||
| 		"fuckadblock": "3.2.1", | ||||
|   | ||||
| @@ -39,7 +39,11 @@ | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<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> | ||||
| 			</sequential-entrance> | ||||
| @@ -53,7 +57,7 @@ | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../i18n'; | ||||
| 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({ | ||||
| 	i18n: i18n('admin/views/drive.vue'), | ||||
| @@ -66,7 +70,7 @@ export default Vue.extend({ | ||||
| 			offset: 0, | ||||
| 			files: [], | ||||
| 			existMore: false, | ||||
| 			faCloud, faTrashAlt | ||||
| 			faCloud, faTrashAlt, faEye, faEyeSlash | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| @@ -132,7 +136,16 @@ export default Vue.extend({ | ||||
| 					text: e.toString() | ||||
| 				}); | ||||
| 			}); | ||||
| 		} | ||||
| 		}, | ||||
|  | ||||
| 		toggleSensitive(file: any) { | ||||
| 			this.$root.api('drive/files/update', { | ||||
| 				fileId: file.id, | ||||
| 				isSensitive: !file.isSensitive | ||||
| 			}); | ||||
|  | ||||
| 			file.isSensitive = !file.isSensitive; | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|   | ||||
| @@ -57,6 +57,15 @@ | ||||
| 			</ui-horizon-group> | ||||
| 			<ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<span slot="desc">{{ $t('smtp-secure-info') }}</span></ui-switch> | ||||
| 		</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> | ||||
| 			<header>summaly Proxy</header> | ||||
| 			<ui-input v-model="summalyProxy">URL</ui-input> | ||||
| @@ -126,7 +135,7 @@ import Vue from 'vue'; | ||||
| import i18n from '../../i18n'; | ||||
| import { url, host } from '../../config'; | ||||
| 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'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| @@ -174,7 +183,10 @@ export default Vue.extend({ | ||||
| 			smtpPort: null, | ||||
| 			smtpUser: 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.smtpUser = meta.smtpUser; | ||||
| 			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, | ||||
| 				smtpPort: parseInt(this.smtpPort, 10), | ||||
| 				smtpUser: this.smtpUser, | ||||
| 				smtpPass: this.smtpPass | ||||
| 				smtpPass: this.smtpPass, | ||||
| 				enableServiceWorker: this.enableServiceWorker, | ||||
| 				swPublicKey: this.swPublicKey, | ||||
| 				swPrivateKey: this.swPrivateKey | ||||
| 			}).then(() => { | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'success', | ||||
|   | ||||
| @@ -22,7 +22,7 @@ export default function(type, data): Notification { | ||||
|  | ||||
| 		case 'unreadMessagingMessage': | ||||
| 			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), | ||||
| 				icon: data.user.avatarUrl | ||||
| 			}; | ||||
| @@ -30,7 +30,7 @@ export default function(type, data): Notification { | ||||
| 		case 'reversiInvited': | ||||
| 			return { | ||||
| 				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 | ||||
| 			}; | ||||
|  | ||||
| @@ -38,21 +38,21 @@ export default function(type, data): Notification { | ||||
| 			switch (data.type) { | ||||
| 				case 'mention': | ||||
| 					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), | ||||
| 						icon: data.user.avatarUrl | ||||
| 					}; | ||||
|  | ||||
| 				case 'reply': | ||||
| 					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), | ||||
| 						icon: data.user.avatarUrl | ||||
| 					}; | ||||
|  | ||||
| 				case 'quote': | ||||
| 					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), | ||||
| 						icon: data.user.avatarUrl | ||||
| 					}; | ||||
|   | ||||
| @@ -15,7 +15,7 @@ export default function(sec) { | ||||
| 	const t | ||||
| 		= tod < 60 ? `${Math.floor(tod)} sec` | ||||
| 		: 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 = ''; | ||||
| 	if (d) str += `${d}, `; | ||||
|   | ||||
| @@ -3,8 +3,8 @@ | ||||
|  | ||||
| export default (data: ArrayBuffer) => { | ||||
| 	//const buf = new Buffer(data); | ||||
| 	//const hash = crypto.createHash("md5"); | ||||
| 	//const hash = crypto.createHash('md5'); | ||||
| 	//hash.update(buf); | ||||
| 	//return hash.digest("hex"); | ||||
| 	//return hash.digest('hex'); | ||||
| 	return ''; | ||||
| }; | ||||
|   | ||||
| @@ -80,8 +80,8 @@ export default (opts: Opts = {}) => ({ | ||||
| 				const ast = parse(this.appearNote.text); | ||||
| 				// TODO: 再帰的にURL要素がないか調べる | ||||
| 				return unique(ast | ||||
| 					.filter(t => ((t.name == 'url' || t.name == 'link') && t.props.url && !t.props.silent)) | ||||
| 					.map(t => t.props.url)); | ||||
| 					.filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) | ||||
| 					.map(t => t.node.props.url)); | ||||
| 			} else { | ||||
| 				return null; | ||||
| 			} | ||||
|   | ||||
| @@ -75,7 +75,7 @@ export default Vue.extend({ | ||||
| 			return this.dark ? '#fff' : '#777'; | ||||
| 		}, | ||||
| 		hHandColor(): string { | ||||
| 			return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--themeColor')).toHexString(); | ||||
| 			return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--primary')).toHexString(); | ||||
| 		}, | ||||
|  | ||||
| 		ms(): number { | ||||
|   | ||||
| @@ -48,7 +48,7 @@ export default Vue.extend({ | ||||
| 		iconAndText(): any[] { | ||||
| 			return ( | ||||
| 				(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 && this.user.isLocked) ? ['plus', this.$t('follow-request')] : | ||||
| 				(!this.isFollowing && !this.user.isLocked) ? ['plus', this.$t('follow')] : | ||||
|   | ||||
| @@ -31,8 +31,8 @@ | ||||
| 						@click="set(i)" | ||||
| 						:title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"> | ||||
| 					<template v-if="!$store.state.settings.games.reversi.useWhiteBlackStones"> | ||||
| 						<img v-if="stone === true" :src="blackUser.avatarUrl" alt="black" :class="{ contrast: $store.state.settings.games.reversi.useContrastStones }"> | ||||
| 						<img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white" :class="{ contrast: $store.state.settings.games.reversi.useContrastStones }"> | ||||
| 						<img v-if="stone === true" :src="blackUser.avatarUrl" alt="black"> | ||||
| 						<img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white"> | ||||
| 					</template> | ||||
| 					<template v-if="$store.state.settings.games.reversi.useWhiteBlackStones"> | ||||
| 						<fa v-if="stone === true" :icon="fasCircle"/> | ||||
| @@ -430,13 +430,6 @@ export default Vue.extend({ | ||||
| 						width 100% | ||||
| 						height 100% | ||||
|  | ||||
| 						&.contrast | ||||
| 							&[alt="black"] | ||||
| 								filter brightness(.5) | ||||
|  | ||||
| 							&[alt="white"] | ||||
| 								filter brightness(2) | ||||
|  | ||||
| 	> .graph | ||||
| 		display grid | ||||
| 		grid-template-columns repeat(61, 1fr) | ||||
|   | ||||
| @@ -52,8 +52,8 @@ export default Vue.extend({ | ||||
| 			if (this.message.text) { | ||||
| 				const ast = parse(this.message.text); | ||||
| 				return unique(ast | ||||
| 					.filter(t => ((t.name == 'url' || t.name == 'link') && t.props.url && !t.silent)) | ||||
| 					.map(t => t.props.url)); | ||||
| 					.filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) | ||||
| 					.map(t => t.node.props.url)); | ||||
| 			} else { | ||||
| 				return null; | ||||
| 			} | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import Vue, { VNode } from 'vue'; | ||||
| import { length } from 'stringz'; | ||||
| import { Node } from '../../../../../mfm/parser'; | ||||
| import { MfmForest } from '../../../../../mfm/parser'; | ||||
| import parse from '../../../../../mfm/parse'; | ||||
| import MkUrl from './url.vue'; | ||||
| import MkMention from './mention.vue'; | ||||
| @@ -9,16 +9,11 @@ import MkFormula from './formula.vue'; | ||||
| import MkGoogle from './google.vue'; | ||||
| import syntaxHighlight from '../../../../../mfm/syntax-highlight'; | ||||
| import { host } from '../../../config'; | ||||
| import { preorderF, countNodesF } from '../../../../../prelude/tree'; | ||||
|  | ||||
| function getTextCount(tokens: Node[]): number { | ||||
| 	const rootCount = sum(tokens.filter(x => x.name === 'text').map(x => length(x.props.text))); | ||||
| 	const childrenCount = sum(tokens.filter(x => x.children).map(x => getTextCount(x.children))); | ||||
| 	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); | ||||
| function sumTextsLength(ts: MfmForest): number { | ||||
| 	const textNodes = preorderF(ts).filter(n => n.type === 'text'); | ||||
| 	return sum(textNodes.map(x => length(x.props.text))); | ||||
| } | ||||
|  | ||||
| export default Vue.component('misskey-flavored-markdown', { | ||||
| @@ -27,10 +22,6 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		ast: { | ||||
| 			type: [], | ||||
| 			required: false | ||||
| 		}, | ||||
| 		shouldBreak: { | ||||
| 			type: Boolean, | ||||
| 			default: true | ||||
| @@ -55,17 +46,15 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 	render(createElement) { | ||||
| 		if (this.text == null || this.text == '') return; | ||||
|  | ||||
| 		const ast = this.ast == null ? | ||||
| 			parse(this.text, this.plainText) : // Parse text to ast | ||||
| 			this.ast as Node[]; | ||||
| 		const ast = parse(this.text, this.plainText); | ||||
|  | ||||
| 		let bigCount = 0; | ||||
| 		let motionCount = 0; | ||||
|  | ||||
| 		const genEl = (ast: Node[]) => concat(ast.map((token): VNode[] => { | ||||
| 			switch (token.name) { | ||||
| 		const genEl = (ast: MfmForest) => concat(ast.map((token): VNode[] => { | ||||
| 			switch (token.node.type) { | ||||
| 				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) { | ||||
| 						const x = text.split('\n') | ||||
| @@ -95,7 +84,7 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
|  | ||||
| 				case 'big': { | ||||
| 					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; | ||||
| 					return (createElement as any)('strong', { | ||||
| 						attrs: { | ||||
| @@ -122,7 +111,7 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
|  | ||||
| 				case 'motion': { | ||||
| 					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; | ||||
| 					return (createElement as any)('span', { | ||||
| 						attrs: { | ||||
| @@ -139,7 +128,7 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 					return [createElement(MkUrl, { | ||||
| 						key: Math.random(), | ||||
| 						props: { | ||||
| 							url: token.props.url, | ||||
| 							url: token.node.props.url, | ||||
| 							target: '_blank', | ||||
| 							style: 'color:var(--mfmLink);' | ||||
| 						} | ||||
| @@ -150,9 +139,9 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 					return [createElement('a', { | ||||
| 						attrs: { | ||||
| 							class: 'link', | ||||
| 							href: token.props.url, | ||||
| 							href: token.node.props.url, | ||||
| 							target: '_blank', | ||||
| 							title: token.props.url, | ||||
| 							title: token.node.props.url, | ||||
| 							style: 'color:var(--mfmLink);' | ||||
| 						} | ||||
| 					}, genEl(token.children))]; | ||||
| @@ -162,8 +151,8 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 					return [createElement(MkMention, { | ||||
| 						key: Math.random(), | ||||
| 						props: { | ||||
| 							host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host, | ||||
| 							username: token.props.username | ||||
| 							host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host, | ||||
| 							username: token.node.props.username | ||||
| 						} | ||||
| 					})]; | ||||
| 				} | ||||
| @@ -172,10 +161,10 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 					return [createElement('router-link', { | ||||
| 						key: Math.random(), | ||||
| 						attrs: { | ||||
| 							to: `/tags/${encodeURIComponent(token.props.hashtag)}`, | ||||
| 							to: `/tags/${encodeURIComponent(token.node.props.hashtag)}`, | ||||
| 							style: 'color:var(--mfmHashtag);' | ||||
| 						} | ||||
| 					}, `#${token.props.hashtag}`)]; | ||||
| 					}, `#${token.node.props.hashtag}`)]; | ||||
| 				} | ||||
|  | ||||
| 				case 'blockCode': { | ||||
| @@ -184,7 +173,7 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 					}, [ | ||||
| 						createElement('code', { | ||||
| 							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': { | ||||
| 					return [createElement('code', { | ||||
| 						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', { | ||||
| 						key: Math.random(), | ||||
| 						attrs: { | ||||
| 							emoji: token.props.emoji, | ||||
| 							name: token.props.name | ||||
| 							emoji: token.node.props.emoji, | ||||
| 							name: token.node.props.name | ||||
| 						}, | ||||
| 						props: { | ||||
| 							customEmojis: this.customEmojis || customEmojis, | ||||
| @@ -242,7 +231,7 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 					return [createElement(MkFormula, { | ||||
| 						key: Math.random(), | ||||
| 						props: { | ||||
| 							formula: token.props.formula | ||||
| 							formula: token.node.props.formula | ||||
| 						} | ||||
| 					})]; | ||||
| 				} | ||||
| @@ -252,13 +241,13 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 					return [createElement(MkGoogle, { | ||||
| 						key: Math.random(), | ||||
| 						props: { | ||||
| 							q: token.props.query | ||||
| 							q: token.node.props.query | ||||
| 						} | ||||
| 					})]; | ||||
| 				} | ||||
|  | ||||
| 				default: { | ||||
| 					console.log('unknown ast type:', token.name); | ||||
| 					console.log('unknown ast type:', token.node.type); | ||||
|  | ||||
| 					return []; | ||||
| 				} | ||||
|   | ||||
| @@ -24,7 +24,7 @@ | ||||
| 			</ui-input> | ||||
|  | ||||
| 			<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> | ||||
| 			</ui-input> | ||||
|  | ||||
|   | ||||
| @@ -1,36 +1,107 @@ | ||||
| <template> | ||||
| <div class="mk-reactions-viewer"> | ||||
| 	<template v-if="reactions"> | ||||
| 		<span :class="{ reacted: note.myReaction == 'like' }" @click="react('like')" v-if="reactions.like" v-particle><mk-reaction-icon reaction="like"/><span>{{ reactions.like }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'love' }" @click="react('love')" v-if="reactions.love" v-particle><mk-reaction-icon reaction="love"/><span>{{ reactions.love }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'laugh' }" @click="react('laugh')" v-if="reactions.laugh" v-particle><mk-reaction-icon reaction="laugh"/><span>{{ reactions.laugh }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'hmm' }" @click="react('hmm')" v-if="reactions.hmm" v-particle><mk-reaction-icon reaction="hmm"/><span>{{ reactions.hmm }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'surprise' }" @click="react('surprise')" v-if="reactions.surprise" v-particle><mk-reaction-icon reaction="surprise"/><span>{{ reactions.surprise }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'congrats' }" @click="react('congrats')" v-if="reactions.congrats" v-particle><mk-reaction-icon reaction="congrats"/><span>{{ reactions.congrats }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'angry' }" @click="react('angry')" v-if="reactions.angry" v-particle><mk-reaction-icon reaction="angry"/><span>{{ reactions.angry }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'confused' }" @click="react('confused')" v-if="reactions.confused" v-particle><mk-reaction-icon reaction="confused"/><span>{{ reactions.confused }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'rip' }" @click="react('rip')" v-if="reactions.rip" v-particle><mk-reaction-icon reaction="rip"/><span>{{ reactions.rip }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'pudding' }" @click="react('pudding')" v-if="reactions.pudding" v-particle><mk-reaction-icon reaction="pudding"/><span>{{ reactions.pudding }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'like' }" @click="react('like')" v-if="reactions.like" v-particle><mk-reaction-icon reaction="like" ref="like"/><span>{{ reactions.like }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'love' }" @click="react('love')" v-if="reactions.love" v-particle><mk-reaction-icon reaction="love" ref="love"/><span>{{ reactions.love }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'laugh' }" @click="react('laugh')" v-if="reactions.laugh" v-particle><mk-reaction-icon reaction="laugh" ref="laugh"/><span>{{ reactions.laugh }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'hmm' }" @click="react('hmm')" v-if="reactions.hmm" v-particle><mk-reaction-icon reaction="hmm" ref="hmm"/><span>{{ reactions.hmm }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'surprise' }" @click="react('surprise')" v-if="reactions.surprise" v-particle><mk-reaction-icon reaction="surprise" ref="surprise"/><span>{{ reactions.surprise }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'congrats' }" @click="react('congrats')" v-if="reactions.congrats" v-particle><mk-reaction-icon reaction="congrats" ref="congrats"/><span>{{ reactions.congrats }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'angry' }" @click="react('angry')" v-if="reactions.angry" v-particle><mk-reaction-icon reaction="angry" ref="angry"/><span>{{ reactions.angry }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'confused' }" @click="react('confused')" v-if="reactions.confused" v-particle><mk-reaction-icon reaction="confused" ref="confused"/><span>{{ reactions.confused }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'rip' }" @click="react('rip')" v-if="reactions.rip" v-particle><mk-reaction-icon reaction="rip" ref="rip"/><span>{{ reactions.rip }}</span></span> | ||||
| 		<span :class="{ reacted: note.myReaction == 'pudding' }" @click="react('pudding')" v-if="reactions.pudding" v-particle><mk-reaction-icon reaction="pudding" ref="pudding"/><span>{{ reactions.pudding }}</span></span> | ||||
| 	</template> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import Icon from './reaction-icon.vue'; | ||||
| import * as anime from 'animejs'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: ['note'], | ||||
| 	computed: { | ||||
| 		reactions(): number { | ||||
| 		reactions(): any { | ||||
| 			return this.note.reactionCounts; | ||||
| 		} | ||||
| 	}, | ||||
| 	watch: { | ||||
| 		'reactions.like'() { | ||||
| 			this.anime('like'); | ||||
| 		}, | ||||
| 		'reactions.love'() { | ||||
| 			this.anime('love'); | ||||
| 		}, | ||||
| 		'reactions.laugh'() { | ||||
| 			this.anime('laugh'); | ||||
| 		}, | ||||
| 		'reactions.hmm'() { | ||||
| 			this.anime('hmm'); | ||||
| 		}, | ||||
| 		'reactions.surprise'() { | ||||
| 			this.anime('surprise'); | ||||
| 		}, | ||||
| 		'reactions.congrats'() { | ||||
| 			this.anime('congrats'); | ||||
| 		}, | ||||
| 		'reactions.angry'() { | ||||
| 			this.anime('angry'); | ||||
| 		}, | ||||
| 		'reactions.confused'() { | ||||
| 			this.anime('confused'); | ||||
| 		}, | ||||
| 		'reactions.rip'() { | ||||
| 			this.anime('rip'); | ||||
| 		}, | ||||
| 		'reactions.pudding'() { | ||||
| 			this.anime('pudding'); | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		react(reaction: string) { | ||||
| 			this.$root.api('notes/reactions/create', { | ||||
| 				noteId: this.note.id, | ||||
| 				reaction: reaction | ||||
| 			}); | ||||
| 		}, | ||||
| 		anime(reaction: string) { | ||||
| 			if (this.$store.state.device.reduceMotion) return; | ||||
| 			if (document.hidden) return; | ||||
|  | ||||
| 			this.$nextTick(() => { | ||||
| 				const rect = this.$refs[reaction].$el.getBoundingClientRect(); | ||||
|  | ||||
| 				const x = rect.left; | ||||
| 				const y = rect.top; | ||||
|  | ||||
| 				const icon = new Icon({ | ||||
| 					parent: this, | ||||
| 					propsData: { | ||||
| 						reaction: reaction | ||||
| 					} | ||||
| 				}).$mount(); | ||||
|  | ||||
| 				icon.$el.style.position = 'absolute'; | ||||
| 				icon.$el.style.zIndex = 100; | ||||
| 				icon.$el.style.top = (y + window.scrollY) + 'px'; | ||||
| 				icon.$el.style.left = (x + window.scrollX) + 'px'; | ||||
| 				icon.$el.style.fontSize = window.getComputedStyle(this.$refs[reaction].$el).fontSize; | ||||
|  | ||||
| 				document.body.appendChild(icon.$el); | ||||
|  | ||||
| 				anime({ | ||||
| 					targets: icon.$el, | ||||
| 					opacity: [1, 0], | ||||
| 					translateY: [0, -64], | ||||
| 					duration: 1000, | ||||
| 					easing: 'linear', | ||||
| 					complete: () => { | ||||
| 						icon.destroyDom(); | ||||
| 					} | ||||
| 				}); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -26,6 +26,7 @@ import { toUnicode } from 'punycode'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('common/views/components/signin.vue'), | ||||
|  | ||||
| 	props: { | ||||
| 		withAvatar: { | ||||
| 			type: Boolean, | ||||
| @@ -33,6 +34,7 @@ export default Vue.extend({ | ||||
| 			default: true | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			signing: false, | ||||
| @@ -45,11 +47,13 @@ export default Vue.extend({ | ||||
| 			meta: null | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.$root.getMeta().then(meta => { | ||||
| 			this.meta = meta; | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		onUsernameChange() { | ||||
| 			this.$root.api('users/show', { | ||||
| @@ -60,6 +64,7 @@ export default Vue.extend({ | ||||
| 				this.user = null; | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onSubmit() { | ||||
| 			this.signing = true; | ||||
|  | ||||
| @@ -80,8 +85,6 @@ export default Vue.extend({ | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
|  | ||||
|  | ||||
| .mk-signin | ||||
| 	color #555 | ||||
|  | ||||
|   | ||||
| @@ -50,6 +50,7 @@ import { toUnicode } from 'punycode'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('common/views/components/signup.vue'), | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			host: toUnicode(host), | ||||
| @@ -64,6 +65,7 @@ export default Vue.extend({ | ||||
| 			meta: null | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		shouldShowProfileUrl(): boolean { | ||||
| 			return (this.username != '' && | ||||
| @@ -72,17 +74,20 @@ export default Vue.extend({ | ||||
| 				this.usernameState != 'max-range'); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.$root.getMeta().then(meta => { | ||||
| 			this.meta = meta; | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		const head = document.getElementsByTagName('head')[0]; | ||||
| 		const script = document.createElement('script'); | ||||
| 		script.setAttribute('src', 'https://www.google.com/recaptcha/api.js'); | ||||
| 		head.appendChild(script); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		onChangeUsername() { | ||||
| 			if (this.username == '') { | ||||
| @@ -111,6 +116,7 @@ export default Vue.extend({ | ||||
| 				this.usernameState = 'error'; | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onChangePassword() { | ||||
| 			if (this.password == '') { | ||||
| 				this.passwordStrength = ''; | ||||
| @@ -120,6 +126,7 @@ export default Vue.extend({ | ||||
| 			const strength = getPasswordStrength(this.password); | ||||
| 			this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; | ||||
| 		}, | ||||
|  | ||||
| 		onChangePasswordRetype() { | ||||
| 			if (this.retypedPassword == '') { | ||||
| 				this.passwordRetypeState = null; | ||||
| @@ -128,6 +135,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 			this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match'; | ||||
| 		}, | ||||
|  | ||||
| 		onSubmit() { | ||||
| 			this.$root.api('signup', { | ||||
| 				username: this.username, | ||||
| @@ -138,8 +146,9 @@ export default Vue.extend({ | ||||
| 				this.$root.api('signin', { | ||||
| 					username: this.username, | ||||
| 					password: this.password | ||||
| 				}, true).then(() => { | ||||
| 					location.href = '/'; | ||||
| 				}, true).then(res => { | ||||
| 					localStorage.setItem('i', res.i); | ||||
| 					location.reload(); | ||||
| 				}); | ||||
| 			}).catch(() => { | ||||
| 				alert(this.$t('some-error')); | ||||
| @@ -154,8 +163,6 @@ export default Vue.extend({ | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
|  | ||||
|  | ||||
| .mk-signup | ||||
| 	min-width 302px | ||||
| </style> | ||||
|   | ||||
| @@ -33,14 +33,7 @@ export default Vue.extend({ | ||||
| 			return typeof this.time == 'string' ? new Date(this.time) : this.time; | ||||
| 		}, | ||||
| 		absolute(): string { | ||||
| 			const time = this._time; | ||||
| 			return ( | ||||
| 				time.getFullYear()    + '年' + | ||||
| 				(time.getMonth() + 1) + '月' + | ||||
| 				time.getDate()        + '日' + | ||||
| 				' ' + | ||||
| 				time.getHours()       + '時' + | ||||
| 				time.getMinutes()     + '分'); | ||||
| 			return this._time.toLocaleString(); | ||||
| 		}, | ||||
| 		relative(): string { | ||||
| 			const time = this._time; | ||||
|   | ||||
| @@ -6,6 +6,7 @@ | ||||
| 			<div class="value" ref="passwordMetar"></div> | ||||
| 		</div> | ||||
| 		<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> | ||||
| 		<template v-if="type != 'file'"> | ||||
| 			<input ref="input" | ||||
| @@ -281,6 +282,20 @@ root(fill) | ||||
| 			transform-origin top left | ||||
| 			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 | ||||
| 			display block | ||||
| 			width 100% | ||||
|   | ||||
							
								
								
									
										150
									
								
								src/client/app/common/views/components/user-list-editor.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								src/client/app/common/views/components/user-list-editor.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -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> | ||||
| 				</header> | ||||
| 				<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> | ||||
|   | ||||
| @@ -25,6 +25,7 @@ class Autocomplete { | ||||
| 	private opts: { | ||||
| 		model: string; | ||||
| 	}; | ||||
| 	private opening: boolean; | ||||
|  | ||||
| 	private get text(): string { | ||||
| 		return this.vm[this.opts.model]; | ||||
| @@ -48,6 +49,7 @@ class Autocomplete { | ||||
| 		this.textarea = textarea; | ||||
| 		this.vm = vm; | ||||
| 		this.opts = opts; | ||||
| 		this.opening = false; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -128,6 +130,8 @@ class Autocomplete { | ||||
| 		if (type != this.currentType) { | ||||
| 			this.close(); | ||||
| 		} | ||||
| 		if (this.opening) return; | ||||
| 		this.opening = true; | ||||
| 		this.currentType = type; | ||||
|  | ||||
| 		//#region サジェストを表示すべき位置を計算 | ||||
| @@ -143,6 +147,8 @@ class Autocomplete { | ||||
| 			this.suggestion.x = x; | ||||
| 			this.suggestion.y = y; | ||||
| 			this.suggestion.q = q; | ||||
|  | ||||
| 			this.opening = false; | ||||
| 		} else { | ||||
| 			const MkAutocomplete = await import('../components/autocomplete.vue').then(m => m.default); | ||||
|  | ||||
| @@ -162,6 +168,8 @@ class Autocomplete { | ||||
|  | ||||
| 			// 要素追加 | ||||
| 			document.body.appendChild(this.suggestion.$el); | ||||
|  | ||||
| 			this.opening = false; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,8 @@ import Particle from '../components/particle.vue'; | ||||
|  | ||||
| export default { | ||||
| 	bind(el, binding, vn) { | ||||
| 		if (vn.context.$store.state.device.reduceMotion) return; | ||||
|  | ||||
| 		el.addEventListener('click', () => { | ||||
| 			const rect = el.getBoundingClientRect(); | ||||
|  | ||||
| @@ -18,9 +20,5 @@ export default { | ||||
|  | ||||
| 			document.body.appendChild(particle.$el); | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
| 	unbind(el, binding, vn) { | ||||
|  | ||||
| 	} | ||||
| }; | ||||
|   | ||||
| @@ -22,7 +22,7 @@ | ||||
| 			:disabled="followWait"> | ||||
| 		<template v-if="!followWait"> | ||||
| 			<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 && user.isLocked"><fa icon="plus"/> {{ $t('follow-request') }}</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 { length } from 'stringz'; | ||||
| import { toASCII } from 'punycode'; | ||||
| import extractMentions from '../../../../../misc/extract-mentions'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/post-form.vue'), | ||||
| @@ -184,8 +185,7 @@ export default Vue.extend({ | ||||
| 		if (this.reply && this.reply.text != null) { | ||||
| 			const ast = parse(this.reply.text); | ||||
|  | ||||
| 			// TODO: 新しいMFMパーサに対応 | ||||
| 			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}`; | ||||
|  | ||||
| 				// 自分は除外 | ||||
|   | ||||
| @@ -137,7 +137,6 @@ | ||||
| 			<section> | ||||
| 				<ui-switch v-model="games_reversi_showBoardLabels">{{ $t('@.show-reversi-board-labels') }}</ui-switch> | ||||
| 				<ui-switch v-model="games_reversi_useWhiteBlackStones">{{ $t('@.use-white-black-reversi-stones') }}</ui-switch> | ||||
| 				<ui-switch v-model="games_reversi_useContrastStones">{{ $t('@.use-contrast-reversi-stones') }}</ui-switch> | ||||
| 			</section> | ||||
| 		</ui-card> | ||||
|  | ||||
| @@ -511,11 +510,6 @@ export default Vue.extend({ | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useWhiteBlackStones', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		games_reversi_useContrastStones: { | ||||
| 			get() { return this.$store.state.settings.games.reversi.useContrastStones; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useContrastStones', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		disableAnimatedMfm: { | ||||
| 			get() { return this.$store.state.settings.disableAnimatedMfm; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); } | ||||
|   | ||||
| @@ -92,6 +92,7 @@ | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import MkUserListsWindow from './user-lists-window.vue'; | ||||
| import MkUserListWindow from './user-list-window.vue'; | ||||
| import MkFollowRequestsWindow from './received-follow-requests-window.vue'; | ||||
| import MkSettingsWindow from './settings-window.vue'; | ||||
| import MkDriveWindow from './drive-window.vue'; | ||||
| @@ -143,7 +144,9 @@ export default Vue.extend({ | ||||
| 			this.close(); | ||||
| 			const w = this.$root.new(MkUserListsWindow); | ||||
| 			w.$once('choosen', list => { | ||||
| 				this.$router.push(`i/lists/${ list.id }`); | ||||
| 				this.$root.new(MkUserListWindow, { | ||||
| 					list | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 		followRequests() { | ||||
|   | ||||
| @@ -14,16 +14,34 @@ export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/ui.header.search.vue'), | ||||
| 	data() { | ||||
| 		return { | ||||
| 			q: '' | ||||
| 			q: '', | ||||
| 			wait: false | ||||
| 		}; | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		onSubmit() { | ||||
| 		async onSubmit() { | ||||
| 			if (this.wait) return; | ||||
|  | ||||
| 			const q = this.q.trim(); | ||||
| 			if (q.startsWith('@')) { | ||||
| 				this.$router.push(`/${q}`); | ||||
| 			} else if (q.startsWith('#')) { | ||||
| 				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 { | ||||
| 				this.$router.push(`/search?q=${encodeURIComponent(q)}`); | ||||
| 			} | ||||
|   | ||||
							
								
								
									
										24
									
								
								src/client/app/desktop/views/components/user-list-window.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/client/app/desktop/views/components/user-list-window.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -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> | ||||
| <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> | ||||
|  | ||||
| 	<div class="xkxvokkjlptzyewouewmceqcxhpgzprp"> | ||||
|   | ||||
| @@ -627,6 +627,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 			> .content | ||||
| 				height 100% | ||||
| 				overflow auto | ||||
|  | ||||
| 	&:not([flexible]) | ||||
| 		> .main > .body > .content | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
| 	<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"> | ||||
| 			<x-note | ||||
| 				:note="note" | ||||
| @@ -24,7 +24,7 @@ | ||||
| 				<span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span> | ||||
| 			</p> | ||||
| 		</template> | ||||
| 	</transition-group> | ||||
| 	</component> | ||||
|  | ||||
| 	<footer v-if="more"> | ||||
| 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| 	<header :class="$style.header"> | ||||
| 		<h1>#{{ $route.params.tag }}</h1> | ||||
| 	</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-ui> | ||||
| </template> | ||||
|   | ||||
| @@ -24,9 +24,14 @@ export default Vue.extend({ | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		const image = [ | ||||
| 			'image/jpeg', | ||||
| 			'image/png', | ||||
| 			'image/gif' | ||||
| 		]; | ||||
| 		this.$root.api('users/notes', { | ||||
| 			userId: this.user.id, | ||||
| 			withFiles: true, | ||||
| 			fileType: image, | ||||
| 			limit: 9, | ||||
| 			untilDate: new Date().getTime() + 1000 * 86400 * 365 | ||||
| 		}).then(notes => { | ||||
|   | ||||
| @@ -123,6 +123,7 @@ import { | ||||
| 	faArrowLeft, | ||||
| 	faMapMarker, | ||||
| 	faRobot, | ||||
| 	faHourglassHalf, | ||||
| } from '@fortawesome/free-solid-svg-icons'; | ||||
|  | ||||
| import { | ||||
| @@ -253,6 +254,7 @@ library.add( | ||||
| 	faArrowLeft, | ||||
| 	faMapMarker, | ||||
| 	faRobot, | ||||
| 	faHourglassHalf, | ||||
|  | ||||
| 	farBell, | ||||
| 	farEnvelope, | ||||
|   | ||||
| @@ -66,6 +66,7 @@ import { host } from '../../../config'; | ||||
| import { erase, unique } from '../../../../../prelude/array'; | ||||
| import { length } from 'stringz'; | ||||
| import { toASCII } from 'punycode'; | ||||
| import extractMentions from '../../../../../misc/extract-mentions'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('mobile/views/components/post-form.vue'), | ||||
| @@ -174,7 +175,7 @@ export default Vue.extend({ | ||||
| 		if (this.reply && this.reply.text != null) { | ||||
| 			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}`; | ||||
|  | ||||
| 				// 自分は除外 | ||||
|   | ||||
| @@ -60,7 +60,8 @@ export default Vue.extend({ | ||||
| 			hasGameInvitation: false, | ||||
| 			connection: null, | ||||
| 			aboutUrl: `/docs/${lang}/about`, | ||||
| 			announcements: [] | ||||
| 			announcements: [], | ||||
| 			searching: false, | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| @@ -95,17 +96,34 @@ export default Vue.extend({ | ||||
|  | ||||
| 	methods: { | ||||
| 		search() { | ||||
| 			if (this.searching) return; | ||||
|  | ||||
| 			this.$root.dialog({ | ||||
| 				title: this.$t('search'), | ||||
| 				input: true | ||||
| 			}).then(({ canceled, result: query }) => { | ||||
| 			}).then(async ({ canceled, result: query }) => { | ||||
| 				if (canceled) return; | ||||
|  | ||||
| 				const q = query.trim(); | ||||
| 				const q = this.q.trim(); | ||||
| 				if (q.startsWith('@')) { | ||||
| 					this.$router.push(`/${q}`); | ||||
| 				} else if (q.startsWith('#')) { | ||||
| 					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 { | ||||
| 					this.$router.push(`/search?q=${encodeURIComponent(q)}`); | ||||
| 				} | ||||
|   | ||||
| @@ -33,7 +33,7 @@ | ||||
|  | ||||
| 				<section> | ||||
| 					<ui-switch v-model="games_reversi_showBoardLabels">{{ $t('@.show-reversi-board-labels') }}</ui-switch> | ||||
| 					<ui-switch v-model="games_reversi_useContrastStones">{{ $t('@.use-contrast-reversi-stones') }}</ui-switch> | ||||
| 					<ui-switch v-model="games_reversi_useWhiteBlackStones">{{ $t('@.use-white-black-reversi-stones') }}</ui-switch> | ||||
| 				</section> | ||||
|  | ||||
| 				<section> | ||||
| @@ -287,9 +287,9 @@ export default Vue.extend({ | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.showBoardLabels', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		games_reversi_useContrastStones: { | ||||
| 			get() { return this.$store.state.settings.games.reversi.useContrastStones; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useContrastStones', value }); } | ||||
| 		games_reversi_useWhiteBlackStones: { | ||||
| 			get() { return this.$store.state.settings.games.reversi.useWhiteBlackStones; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useWhiteBlackStones', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		disableAnimatedMfm: { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| 	<span slot="header"><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</span> | ||||
|  | ||||
| 	<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"/> | ||||
| 	</main> | ||||
| </mk-ui> | ||||
|   | ||||
| @@ -3,11 +3,7 @@ | ||||
| 	<span slot="header" v-if="!fetching"><fa icon="list"/>{{ list.title }}</span> | ||||
|  | ||||
| 	<main v-if="!fetching"> | ||||
| 		<ul> | ||||
| 			<li v-for="user in users" :key="user.id"><router-link :to="user | userPage"> | ||||
| 				<mk-user-name :user="user"/> | ||||
| 			</router-link></li> | ||||
| 		</ul> | ||||
| 		<x-editor :list="list"/> | ||||
| 	</main> | ||||
| </mk-ui> | ||||
| </template> | ||||
| @@ -15,13 +11,16 @@ | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import Progress from '../../../common/scripts/loading'; | ||||
| import XEditor from '../../../common/views/components/user-list-editor.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XEditor | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			list: null, | ||||
| 			users: null | ||||
| 			list: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	watch: { | ||||
| @@ -42,12 +41,6 @@ export default Vue.extend({ | ||||
| 				this.fetching = false; | ||||
|  | ||||
| 				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> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
|  | ||||
|  | ||||
| main | ||||
| 	width 100% | ||||
| 	max-width 680px | ||||
|   | ||||
| @@ -26,10 +26,15 @@ export default Vue.extend({ | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		const image = [ | ||||
| 			'image/jpeg', | ||||
| 			'image/png', | ||||
| 			'image/gif' | ||||
| 		]; | ||||
| 		this.$root.api('users/notes', { | ||||
| 			userId: this.user.id, | ||||
| 			withFiles: true, | ||||
| 			limit: 6, | ||||
| 			fileType: image, | ||||
| 			limit: 9, | ||||
| 			untilDate: new Date().getTime() + 1000 * 86400 * 365 | ||||
| 		}).then(notes => { | ||||
| 			for (const note of notes) { | ||||
|   | ||||
| @@ -40,8 +40,7 @@ const defaultSettings = { | ||||
| 	games: { | ||||
| 		reversi: { | ||||
| 			showBoardLabels: false, | ||||
| 			useWhileBlackStones: false, | ||||
| 			useContrastStones: false | ||||
| 			useWhiteBlackStones: false, | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|   | ||||
							
								
								
									
										2
									
								
								src/client/app/v.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/client/app/v.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| declare module "*.vue" { | ||||
| declare module '*.vue' { | ||||
| 	import Vue from 'vue'; | ||||
| 	export default Vue; | ||||
| } | ||||
|   | ||||
| @@ -39,21 +39,7 @@ export type Source = { | ||||
|  | ||||
| 	accesslog?: string; | ||||
|  | ||||
| 	/** | ||||
| 	 * Service Worker | ||||
| 	 */ | ||||
| 	sw?: { | ||||
| 		public_key: string; | ||||
| 		private_key: string; | ||||
| 	}; | ||||
|  | ||||
| 	clusterLimit?: number; | ||||
|  | ||||
| 	user_recommendation?: { | ||||
| 		external: boolean; | ||||
| 		engine: string; | ||||
| 		timeout: number; | ||||
| 	}; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { count, concat } from "../../prelude/array"; | ||||
| import { count, concat } from '../../prelude/array'; | ||||
|  | ||||
| // MISSKEY REVERSI ENGINE | ||||
|  | ||||
| @@ -76,27 +76,14 @@ export default class Reversi { | ||||
| 		this.mapHeight = map.length; | ||||
| 		const mapData = map.join(''); | ||||
|  | ||||
| 		this.board = mapData.split('').map(d => { | ||||
| 			if (d == '-') return null; | ||||
| 			if (d == 'b') return BLACK; | ||||
| 			if (d == 'w') return WHITE; | ||||
| 			return undefined; | ||||
| 		}); | ||||
| 		this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined); | ||||
|  | ||||
| 		this.map = mapData.split('').map(d => { | ||||
| 			if (d == '-' || d == 'b' || d == 'w') return 'empty'; | ||||
| 			return 'null'; | ||||
| 		}); | ||||
| 		this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null'); | ||||
| 		//#endregion | ||||
|  | ||||
| 		// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある | ||||
| 		if (!this.canPutSomewhere(BLACK)) { | ||||
| 			if (!this.canPutSomewhere(WHITE)) { | ||||
| 				this.turn = null; | ||||
| 			} else { | ||||
| 				this.turn = WHITE; | ||||
| 			} | ||||
| 		} | ||||
| 		if (!this.canPutSomewhere(BLACK)) | ||||
| 			this.turn = this.canPutSomewhere(WHITE) ? WHITE : null; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -117,16 +104,14 @@ export default class Reversi { | ||||
| 	 * 黒石の比率 | ||||
| 	 */ | ||||
| 	public get blackP() { | ||||
| 		if (this.blackCount == 0 && this.whiteCount == 0) return 0; | ||||
| 		return this.blackCount / (this.blackCount + this.whiteCount); | ||||
| 		return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.blackCount / (this.blackCount + this.whiteCount); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * 白石の比率 | ||||
| 	 */ | ||||
| 	public get whiteP() { | ||||
| 		if (this.blackCount == 0 && this.whiteCount == 0) return 0; | ||||
| 		return this.whiteCount / (this.blackCount + this.whiteCount); | ||||
| 		return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.whiteCount / (this.blackCount + this.whiteCount); | ||||
| 	} | ||||
|  | ||||
| 	public transformPosToXy(pos: number): number[] { | ||||
| @@ -172,13 +157,10 @@ export default class Reversi { | ||||
|  | ||||
| 	private calcTurn() { | ||||
| 		// ターン計算 | ||||
| 		if (this.canPutSomewhere(!this.prevColor)) { | ||||
| 			this.turn = !this.prevColor; | ||||
| 		} else if (this.canPutSomewhere(this.prevColor)) { | ||||
| 			this.turn = this.prevColor; | ||||
| 		} else { | ||||
| 			this.turn = null; | ||||
| 		} | ||||
| 		this.turn = | ||||
| 			this.canPutSomewhere(!this.prevColor) ? !this.prevColor : | ||||
| 			this.canPutSomewhere(this.prevColor) ? this.prevColor : | ||||
| 			null; | ||||
| 	} | ||||
|  | ||||
| 	public undo() { | ||||
| @@ -199,8 +181,7 @@ export default class Reversi { | ||||
| 	 */ | ||||
| 	public mapDataGet(pos: number): MapPixel { | ||||
| 		const [x, y] = this.transformPosToXy(pos); | ||||
| 		if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) return 'null'; | ||||
| 		return this.map[pos]; | ||||
| 		return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos]; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -223,16 +204,10 @@ export default class Reversi { | ||||
| 	 * @param pos 位置 | ||||
| 	 */ | ||||
| 	public canPut(color: Color, pos: number): boolean { | ||||
| 		// 既に石が置いてある場所には打てない | ||||
| 		if (this.board[pos] !== null) return false; | ||||
|  | ||||
| 		if (this.opts.canPutEverywhere) { | ||||
| 			// 挟んでなくても置けるモード | ||||
| 			return this.mapDataGet(pos) == 'empty'; | ||||
| 		} else { | ||||
| 			// 相手の石を1つでも反転させられるか | ||||
| 			return this.effects(color, pos).length !== 0; | ||||
| 		} | ||||
| 		return ( | ||||
| 			this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない | ||||
| 			this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード | ||||
| 			this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -263,19 +238,13 @@ export default class Reversi { | ||||
| 				[x, y] = nextPos(x, y); | ||||
|  | ||||
| 				// 座標が指し示す位置がボード外に出たとき | ||||
| 				if (this.opts.loopedBoard) { | ||||
| 					x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth; | ||||
| 					y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight; | ||||
|  | ||||
| 					if (this.transformXyToPos(x, y) == initPos) { | ||||
| 				if (this.opts.loopedBoard && this.transformXyToPos( | ||||
| 					(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth), | ||||
| 					(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) == initPos) | ||||
| 						// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ) | ||||
| 						return found; | ||||
| 					} | ||||
| 				} else { | ||||
| 					if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) { | ||||
| 						return []; // 挟めないことが確定 (盤面外に到達) | ||||
| 					} | ||||
| 				} | ||||
| 					return found; | ||||
| 				else if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) | ||||
| 					return []; // 挟めないことが確定 (盤面外に到達) | ||||
|  | ||||
| 				const pos = this.transformXyToPos(x, y); | ||||
| 				if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達) | ||||
| @@ -300,14 +269,9 @@ export default class Reversi { | ||||
| 	 * ゲームの勝者 (null = 引き分け) | ||||
| 	 */ | ||||
| 	public get winner(): Color { | ||||
| 		if (!this.isEnded) return undefined; | ||||
|  | ||||
| 		if (this.blackCount == this.whiteCount) return null; | ||||
|  | ||||
| 		if (this.opts.isLlotheo) { | ||||
| 			return this.blackCount > this.whiteCount ? WHITE : BLACK; | ||||
| 		} else { | ||||
| 			return this.blackCount > this.whiteCount ? BLACK : WHITE; | ||||
| 		} | ||||
| 		return this.isEnded ? | ||||
| 			this.blackCount == this.whiteCount ? null : | ||||
| 			this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK : | ||||
| 			undefined; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -2,10 +2,10 @@ const jsdom = require('jsdom'); | ||||
| const { JSDOM } = jsdom; | ||||
| import config from '../config'; | ||||
| import { INote } from '../models/note'; | ||||
| import { Node } from './parser'; | ||||
| 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) { | ||||
| 		return null; | ||||
| 	} | ||||
| @@ -14,11 +14,11 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser | ||||
|  | ||||
| 	const doc = window.document; | ||||
|  | ||||
| 	function appendChildren(children: Node[], targetElement: any): void { | ||||
| 		for (const child of children.map(n => handlers[n.name](n))) targetElement.appendChild(child); | ||||
| 	function appendChildren(children: MfmForest, targetElement: any): void { | ||||
| 		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) { | ||||
| 			const el = doc.createElement('b'); | ||||
| 			appendChildren(token.children, el); | ||||
| @@ -58,7 +58,7 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser | ||||
| 		blockCode(token) { | ||||
| 			const pre = doc.createElement('pre'); | ||||
| 			const inner = doc.createElement('code'); | ||||
| 			inner.innerHTML = token.props.code; | ||||
| 			inner.innerHTML = token.node.props.code; | ||||
| 			pre.appendChild(inner); | ||||
| 			return pre; | ||||
| 		}, | ||||
| @@ -70,39 +70,39 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser | ||||
| 		}, | ||||
|  | ||||
| 		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) { | ||||
| 			const a = doc.createElement('a'); | ||||
| 			a.href = `${config.url}/tags/${token.props.hashtag}`; | ||||
| 			a.textContent = `#${token.props.hashtag}`; | ||||
| 			a.href = `${config.url}/tags/${token.node.props.hashtag}`; | ||||
| 			a.textContent = `#${token.node.props.hashtag}`; | ||||
| 			a.setAttribute('rel', 'tag'); | ||||
| 			return a; | ||||
| 		}, | ||||
|  | ||||
| 		inlineCode(token) { | ||||
| 			const el = doc.createElement('code'); | ||||
| 			el.textContent = token.props.code; | ||||
| 			el.textContent = token.node.props.code; | ||||
| 			return el; | ||||
| 		}, | ||||
|  | ||||
| 		math(token) { | ||||
| 			const el = doc.createElement('code'); | ||||
| 			el.textContent = token.props.formula; | ||||
| 			el.textContent = token.node.props.formula; | ||||
| 			return el; | ||||
| 		}, | ||||
|  | ||||
| 		link(token) { | ||||
| 			const a = doc.createElement('a'); | ||||
| 			a.href = token.props.url; | ||||
| 			a.href = token.node.props.url; | ||||
| 			appendChildren(token.children, a); | ||||
| 			return a; | ||||
| 		}, | ||||
|  | ||||
| 		mention(token) { | ||||
| 			const a = doc.createElement('a'); | ||||
| 			const { username, host, acct } = token.props; | ||||
| 			const { username, host, acct } = token.node.props; | ||||
| 			switch (host) { | ||||
| 				case 'github.com': | ||||
| 					a.href = `https://github.com/${username}`; | ||||
| @@ -133,7 +133,7 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser | ||||
|  | ||||
| 		text(token) { | ||||
| 			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)) { | ||||
| 				el.appendChild(x === 'br' ? doc.createElement('br') : x); | ||||
| @@ -144,15 +144,15 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser | ||||
|  | ||||
| 		url(token) { | ||||
| 			const a = doc.createElement('a'); | ||||
| 			a.href = token.props.url; | ||||
| 			a.textContent = token.props.url; | ||||
| 			a.href = token.node.props.url; | ||||
| 			a.textContent = token.node.props.url; | ||||
| 			return a; | ||||
| 		}, | ||||
|  | ||||
| 		search(token) { | ||||
| 			const a = doc.createElement('a'); | ||||
| 			a.href = `https://www.google.com/?#q=${token.props.query}`; | ||||
| 			a.textContent = token.props.content; | ||||
| 			a.href = `https://www.google.com/?#q=${token.node.props.query}`; | ||||
| 			a.textContent = token.node.props.content; | ||||
| 			return a; | ||||
| 		} | ||||
| 	}; | ||||
|   | ||||
| @@ -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 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 == '') { | ||||
| 		return null; | ||||
| 	} | ||||
|  | ||||
| 	let nodes: Node[] = plainText ? plainParser.root.tryParse(source) : parser.root.tryParse(source); | ||||
|  | ||||
| 	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; | ||||
| 	const raw = plainText ? plainParser.root.tryParse(source) : parser.root.tryParse(source) as MfmForest; | ||||
| 	return removeEmptyTextNodes(concatConsecutiveTextTrees(raw)); | ||||
| }; | ||||
|   | ||||
										
											
												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) { | ||||
| 	return text | ||||
| @@ -307,7 +307,7 @@ const elements: Element[] = [ | ||||
| 	} | ||||
| ]; | ||||
|  | ||||
| // specify lang is todo | ||||
| // TODO: specify lang | ||||
| export default (source: string, lang?: string): string => { | ||||
| 	let code = source; | ||||
| 	let html = ''; | ||||
|   | ||||
							
								
								
									
										9
									
								
								src/misc/extract-emojis.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/misc/extract-emojis.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
| } | ||||
							
								
								
									
										9
									
								
								src/misc/extract-hashtags.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/misc/extract-hashtags.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
| } | ||||
							
								
								
									
										10
									
								
								src/misc/extract-mentions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/misc/extract-mentions.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||
| 	enableDiscordIntegration: 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, | ||||
| 	errorImageUrl: 'https://ai.misskey.xyz/aiart/yubitun.png' | ||||
| 	errorImageUrl: 'https://ai.misskey.xyz/aiart/yubitun.png', | ||||
| 	enableServiceWorker: false | ||||
| }; | ||||
|  | ||||
| export default async function(): Promise<IMeta> { | ||||
|   | ||||
| @@ -14,7 +14,11 @@ const summarize = (note: any): string => { | ||||
| 	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) { | ||||
|   | ||||
| @@ -15,4 +15,6 @@ export type IEmoji = { | ||||
| 	url: string; | ||||
| 	aliases?: string[]; | ||||
| 	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 = { | ||||
| 	name?: string; | ||||
| @@ -223,4 +236,8 @@ export type IMeta = { | ||||
| 	smtpPort?: number; | ||||
| 	smtpUser?: string; | ||||
| 	smtpPass?: string; | ||||
|  | ||||
| 	enableServiceWorker?: boolean; | ||||
| 	swPublicKey?: string; | ||||
| 	swPrivateKey?: string; | ||||
| }; | ||||
|   | ||||
| @@ -155,7 +155,7 @@ export const isRemoteUser = (user: any): user is IRemoteUser => | ||||
|  | ||||
| //#region Validators | ||||
| 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 { | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|  | ||||
| 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[] { | ||||
| 	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[] { | ||||
| 	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. | ||||
|  * The order of result values are determined by the first array. | ||||
|  */ | ||||
| export function difference<T>(includes: T[], excludes: T[]): T[] { | ||||
| 	return includes.filter(x => !excludes.includes(x)); | ||||
| export function difference<T>(xs: T[], ys: T[]): T[] { | ||||
| 	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[] { | ||||
| 	return [...new Set(xs)]; | ||||
| } | ||||
| @@ -38,7 +60,11 @@ export function maximum(xs: number[]): number { | ||||
| 	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[][]; | ||||
| 	for (const x of xs) { | ||||
| 		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; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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[][] { | ||||
| 	return groupBy((a, b) => f(a) === f(b), xs); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Compare two arrays by lexicographical order | ||||
|  */ | ||||
| export function lessThan(xs: number[], ys: number[]): boolean { | ||||
| 	for (let i = 0; i < Math.min(xs.length, ys.length); i++) { | ||||
| 		if (xs[i] < ys[i]) return true; | ||||
| @@ -62,7 +95,10 @@ export function lessThan(xs: number[], ys: number[]): boolean { | ||||
| 	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 = []; | ||||
| 	for (const x of xs) { | ||||
| 		if (f(x)) { | ||||
| @@ -73,3 +109,9 @@ export function takeWhile<T>(f: (x: T) => boolean, xs: T[]): T[] { | ||||
| 	} | ||||
| 	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; | ||||
| } | ||||
|   | ||||
							
								
								
									
										5
									
								
								src/prelude/relation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/prelude/relation.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 { | ||||
| 	return xs.reduce((a, b) => a + b, ""); | ||||
| 	return xs.reduce((a, b) => a + b, ''); | ||||
| } | ||||
|  | ||||
| export function capitalize(s: string): string { | ||||
|   | ||||
							
								
								
									
										36
									
								
								src/prelude/tree.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/prelude/tree.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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)); | ||||
| } | ||||
| @@ -2,17 +2,26 @@ const push = require('web-push'); | ||||
| import * as mongo from 'mongodb'; | ||||
| import Subscription from './models/sw-subscription'; | ||||
| import config from './config'; | ||||
| import fetchMeta from './misc/fetch-meta'; | ||||
| import { IMeta } from './models/meta'; | ||||
|  | ||||
| if (config.sw) { | ||||
| 	// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 | ||||
| 	push.setVapidDetails( | ||||
| 		config.url, | ||||
| 		config.sw.public_key, | ||||
| 		config.sw.private_key); | ||||
| } | ||||
| let meta: IMeta = null; | ||||
|  | ||||
| setInterval(() => { | ||||
| 	fetchMeta().then(m => { | ||||
| 		meta = m; | ||||
|  | ||||
| 		if (meta.enableServiceWorker) { | ||||
| 			// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 | ||||
| 			push.setVapidDetails(config.url, | ||||
| 				meta.swPublicKey, | ||||
| 				meta.swPrivateKey); | ||||
| 		} | ||||
| 	}); | ||||
| }, 3000); | ||||
|  | ||||
| export default async function(userId: mongo.ObjectID | string, type: string, body?: any) { | ||||
| 	if (!config.sw) return; | ||||
| 	if (!meta.enableServiceWorker) return; | ||||
|  | ||||
| 	if (typeof userId === 'string') { | ||||
| 		userId = new mongo.ObjectID(userId); | ||||
|   | ||||
| @@ -181,6 +181,20 @@ export async function extractEmojis(tags: ITag[], host_: string) { | ||||
| 			}); | ||||
|  | ||||
| 			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; | ||||
| 			} | ||||
|  | ||||
| @@ -189,8 +203,10 @@ export async function extractEmojis(tags: ITag[], host_: string) { | ||||
| 			return await Emoji.insert({ | ||||
| 				host, | ||||
| 				name, | ||||
| 				uri: tag.id, | ||||
| 				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 { IEmoji } from '../../../models/emoji'; | ||||
| import { ITag } from './tag'; | ||||
| import Following from '../../../models/following'; | ||||
|  | ||||
| const log = debug('misskey:activitypub'); | ||||
|  | ||||
| @@ -164,7 +165,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU | ||||
| 				publicKeyPem: person.publicKey.publicKeyPem | ||||
| 			}, | ||||
| 			inbox: person.inbox, | ||||
| 			sharedInbox: person.sharedInbox, | ||||
| 			sharedInbox: person.sharedInbox || person.endpoints ? person.endpoints.sharedInbox : undefined, | ||||
| 			featured: person.featured, | ||||
| 			endpoints: person.endpoints, | ||||
| 			uri: person.id, | ||||
| @@ -340,7 +341,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje | ||||
| 		$set: { | ||||
| 			lastFetchedAt: new Date(), | ||||
| 			inbox: person.inbox, | ||||
| 			sharedInbox: person.sharedInbox, | ||||
| 			sharedInbox: person.sharedInbox || person.endpoints ? person.endpoints.sharedInbox : undefined, | ||||
| 			featured: person.featured, | ||||
| 			avatarId: avatar ? avatar._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)); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { IIcon } from "./icon"; | ||||
| import { IIcon } from './icon'; | ||||
|  | ||||
| /*** | ||||
|  * tag (ActivityPub) | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import config from '../../../config'; | ||||
| import { ILocalUser, IRemoteUser } from "../../../models/user"; | ||||
| import { ILocalUser, IRemoteUser } from '../../../models/user'; | ||||
|  | ||||
| export default (blocker?: ILocalUser, blockee?: IRemoteUser) => ({ | ||||
| 	type: 'Block', | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import config from '../../../config'; | ||||
| import { ILocalUser } from "../../../models/user"; | ||||
| import { ILocalUser } from '../../../models/user'; | ||||
|  | ||||
| export default (object: any, user: ILocalUser) => ({ | ||||
| 	type: 'Delete', | ||||
|   | ||||
| @@ -102,10 +102,9 @@ export default async function renderNote(note: INote, dive = true): Promise<any> | ||||
|  | ||||
| 	let apText = text; | ||||
|  | ||||
| 	if (note.renoteId != null) { | ||||
| 	if (quote) { | ||||
| 		if (apText == null) apText = ''; | ||||
| 		const url = `${config.url}/notes/${note.renoteId}`; | ||||
| 		apText += `\n\nRE: ${url}`; | ||||
| 		apText += `\n\nRE: ${quote}`; | ||||
| 	} | ||||
|  | ||||
| 	const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; | ||||
|   | ||||
| @@ -63,6 +63,7 @@ export default async (user: ILocalUser) => { | ||||
| 		following: `${id}/following`, | ||||
| 		featured: `${id}/collections/featured`, | ||||
| 		sharedInbox: `${config.url}/inbox`, | ||||
| 		endpoints: { sharedInbox: `${config.url}/inbox` }, | ||||
| 		url: `${config.url}/@${user.username}`, | ||||
| 		preferredUsername: user.username, | ||||
| 		name: user.name, | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import config from '../../../config'; | ||||
| import { ILocalUser, IUser } from "../../../models/user"; | ||||
| import { ILocalUser, IUser } from '../../../models/user'; | ||||
|  | ||||
| export default (object: any, user: ILocalUser | IUser) => ({ | ||||
| 	type: 'Undo', | ||||
|   | ||||
| @@ -56,7 +56,7 @@ export interface IPerson extends IObject { | ||||
| 	following: any; | ||||
| 	featured?: any; | ||||
| 	outbox: any; | ||||
| 	endpoints: string[]; | ||||
| 	endpoints: any; | ||||
| } | ||||
|  | ||||
| export const isCollection = (object: IObject): object is ICollection => | ||||
|   | ||||
| @@ -7,9 +7,11 @@ import { createHttpJob } from '../queue'; | ||||
| import pack from '../remote/activitypub/renderer'; | ||||
| import Note from '../models/note'; | ||||
| import User, { isLocalUser, ILocalUser, IUser } from '../models/user'; | ||||
| import Emoji from '../models/emoji'; | ||||
| import renderNote from '../remote/activitypub/renderer/note'; | ||||
| import renderKey from '../remote/activitypub/renderer/key'; | ||||
| import renderPerson from '../remote/activitypub/renderer/person'; | ||||
| import renderEmoji from '../remote/activitypub/renderer/emoji'; | ||||
| import Outbox, { packActivity } from './activitypub/outbox'; | ||||
| import Followers from './activitypub/followers'; | ||||
| import Following from './activitypub/following'; | ||||
| @@ -188,4 +190,21 @@ router.get('/@:user', async (ctx, next) => { | ||||
| }); | ||||
| //#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; | ||||
|   | ||||
| @@ -285,6 +285,27 @@ export const meta = { | ||||
| 				'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; | ||||
| 	} | ||||
|  | ||||
| 	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({}, { | ||||
| 		$set: set | ||||
| 	}, { upsert: true }); | ||||
|   | ||||
| @@ -25,11 +25,10 @@ export const meta = { | ||||
| 	}, | ||||
| }; | ||||
|  | ||||
| export default define(meta, (ps) => new Promise(async (res, rej) => { | ||||
| 	const object = await fetchAny(ps.uri); | ||||
| 	if (object == null) return rej('object not found'); | ||||
|  | ||||
| 	res(object); | ||||
| export default define(meta, (ps) => new Promise((res, rej) => { | ||||
| 	fetchAny(ps.uri) | ||||
| 		.then(object => object != null ? res(object) : rej('object not found')) | ||||
| 		.catch(e => rej(e)); | ||||
| })); | ||||
|  | ||||
| /*** | ||||
|   | ||||
| @@ -57,14 +57,17 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { | ||||
| 	// Fetch file | ||||
| 	const file = await DriveFile | ||||
| 		.findOne({ | ||||
| 			_id: ps.fileId, | ||||
| 			'metadata.userId': user._id | ||||
| 			_id: ps.fileId | ||||
| 		}); | ||||
|  | ||||
| 	if (file === null) { | ||||
| 		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.isSensitive !== undefined) file.metadata.isSensitive = ps.isSensitive; | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import { publishToFollowers } from '../../../../services/i/update'; | ||||
| import define from '../../define'; | ||||
| import getDriveFileUrl from '../../../../misc/get-drive-file-url'; | ||||
| import parse from '../../../../mfm/parse'; | ||||
| import { extractEmojis } from '../../../../services/note/create'; | ||||
| import extractEmojis from '../../../../misc/extract-emojis'; | ||||
| const langmap = require('langmap'); | ||||
|  | ||||
| export const meta = { | ||||
|   | ||||
| @@ -63,7 +63,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { | ||||
| 		cacheRemoteFiles: instance.cacheRemoteFiles, | ||||
| 		enableRecaptcha: instance.enableRecaptcha, | ||||
| 		recaptchaSiteKey: instance.recaptchaSiteKey, | ||||
| 		swPublickey: config.sw ? config.sw.public_key : null, | ||||
| 		swPublickey: instance.swPublicKey, | ||||
| 		bannerUrl: instance.bannerUrl, | ||||
| 		errorImageUrl: instance.errorImageUrl, | ||||
| 		maxNoteTextLength: instance.maxNoteTextLength, | ||||
| @@ -85,7 +85,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { | ||||
| 			twitter: instance.enableTwitterIntegration, | ||||
| 			github: instance.enableGithubIntegration, | ||||
| 			discord: instance.enableDiscordIntegration, | ||||
| 			serviceWorker: config.sw ? true : false, | ||||
| 			serviceWorker: instance.enableServiceWorker, | ||||
| 			userRecommendation: { | ||||
| 				external: instance.enableExternalUserRecommendation, | ||||
| 				engine: instance.externalUserRecommendationEngine, | ||||
| @@ -114,6 +114,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { | ||||
| 		response.smtpPort = instance.smtpPort; | ||||
| 		response.smtpUser = instance.smtpUser; | ||||
| 		response.smtpPass = instance.smtpPass; | ||||
| 		response.swPrivateKey = instance.swPrivateKey; | ||||
| 	} | ||||
|  | ||||
| 	res(response); | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user