Merge branch 'master' into l10n_master
| @@ -1,67 +1,136 @@ | ||||
| # インスタンス名 | ||||
| name: | ||||
| name: example-instance-name # Name of your instance | ||||
| description: example-description # Description of your instance | ||||
|  | ||||
| # インスタンスの紹介 | ||||
| description: | ||||
|  | ||||
| # サーバーのメンテナ情報 | ||||
| maintainer: | ||||
|   # メンテナの名前 | ||||
|   name: | ||||
|   name: example-maitainer-name # Your name | ||||
|   url: http://example.com/ # Your contact (http or mailto) | ||||
|   repository_url: https://github.com/syuilo/misskey # Repository URL | ||||
|   feedback_url: https://github.com/syuilo/misskey/issues # Feedback URL (e.g. github issue) | ||||
|  | ||||
|   # メンテナの連絡先(URLかmailto形式のURL) | ||||
|   url: | ||||
| # URL and Port settings overview | ||||
| # e.g., If you want to realize following structure: | ||||
| # | ||||
| #               +--- https://example.com:123 ----------+ | ||||
| # +------+      |+-------------+      +---------------+| | ||||
| # | User | ---> || Proxy (123) | ---> | Misskey (456) || | ||||
| # +------+      |+-------------+      +---------------+| | ||||
| #               +--------------------------------------+ | ||||
| # | ||||
| # You need to set 'https://example.com:123' to 'url' prop and | ||||
| # You need to set 456 to 'port' prop. | ||||
| # | ||||
| # In other words, the 'url' prop should be the final accessible URL seen by a user. | ||||
| # 'port' prop is a port that the Misskey server should actually listen | ||||
| # on and it is not necessarily the port that a user accesses. | ||||
|  | ||||
| # (Misskeyを動かす)URL | ||||
| url: | ||||
| url: http://localhost/ | ||||
|  | ||||
| # 待受ポート | ||||
| port: | ||||
| # A port that your Misskey server should listen. | ||||
| # This value is not a port to use when accessing with a browser. | ||||
| port: 80 | ||||
|  | ||||
| # TLSの設定(利用しない場合は省略してください) | ||||
| https: | ||||
|   # 証明書のパス... | ||||
|   key: | ||||
|   cert: | ||||
|  | ||||
| # MongoDBの設定 | ||||
| mongodb: | ||||
|   host: localhost | ||||
|   port: 27017 | ||||
|   db: misskey | ||||
|   user: | ||||
|   pass: | ||||
|   user: example-misskey-user | ||||
|   pass: example-misskey-pass | ||||
|  | ||||
| # Redisの設定 | ||||
| redis: | ||||
|   host: localhost | ||||
|   port: 6379 | ||||
|   pass: | ||||
|   pass: example-pass | ||||
|  | ||||
| # reCAPTCHAの設定 | ||||
| recaptcha: | ||||
|   site_key: | ||||
|   secret_key: | ||||
| # Drive capacity of a local user (MB) | ||||
| localDriveCapacityMb: 256 | ||||
|  | ||||
| # ServiceWrokerの設定 | ||||
| sw: | ||||
|   # VAPIDの公開鍵 | ||||
|   public_key: | ||||
| # Drive capacity of a remote user (MB) | ||||
| remoteDriveCapacityMb: 8 | ||||
|  | ||||
|   # VAPIDの秘密鍵 | ||||
|   private_key: | ||||
|  | ||||
| # Google Maps API | ||||
| google_maps_api_key: | ||||
|  | ||||
| # Twitterインテグレーションの設定(利用しない場合は省略可能) | ||||
| twitter: | ||||
|   # インテグレーション用アプリのコンシューマーキー | ||||
|   consumer_key: | ||||
|  | ||||
|   # インテグレーション用アプリのコンシューマーシークレット | ||||
|   consumer_secret: | ||||
|  | ||||
| # true にすると、リモートのファイルをキャッシュしなくなります(直リンクします)。 | ||||
| # ストレージ容量を節約することができますが、「リモートメディアを表示しない」設定をオンにしているユーザーは、リモートの画像などは見えなくなります。 | ||||
| # If enabled: | ||||
| #  Server will not cache remote files (Using direct link instead). | ||||
| #  You can save your storage. | ||||
| #  Users cannot see remote images when they turn off "Show media from a remote server" setting. | ||||
| preventCache: false | ||||
|  | ||||
| drive: | ||||
|   storage: 'db' | ||||
|  | ||||
|   # OR | ||||
|  | ||||
|   # storage: 'minio' | ||||
|   # bucket: | ||||
|   # prefix: | ||||
|   # config: | ||||
|   #   endPoint: | ||||
|   #   port: | ||||
|   #   secure: | ||||
|   #   accessKey: | ||||
|   #   secretKey: | ||||
|  | ||||
|   # S3 example | ||||
|   # storage: 'minio' | ||||
|   # bucket: bucket-name | ||||
|   # prefix: files | ||||
|   # config: | ||||
|   #   endPoint: s3-us-west-2.amazonaws.com | ||||
|   #   region: us-west-2 | ||||
|   #   secure: true | ||||
|   #   accessKey: XXX | ||||
|   #   secretKey: YYY | ||||
|  | ||||
|   # S3 example (with CDN, custom domain) | ||||
|   # storage: 'minio' | ||||
|   # bucket: drive.example.com | ||||
|   # prefix: files | ||||
|   # baseUrl: https://drive.example.com | ||||
|   # config: | ||||
|   #   endPoint: s3-us-west-2.amazonaws.com | ||||
|   #   region: us-west-2 | ||||
|   #   secure: true | ||||
|   #   accessKey: XXX | ||||
|   #   secretKey: YYY | ||||
|  | ||||
| # | ||||
| # Below settings are optional | ||||
| # | ||||
|  | ||||
| # TLS | ||||
| # https: | ||||
| #   # path for certification | ||||
| #   key: example-tls-key | ||||
| #   cert: example-tls-cert | ||||
|  | ||||
| # Elasticsearch | ||||
| # elasticsearch: | ||||
| #   host: localhost | ||||
| #   port: 9200 | ||||
| #   pass: null | ||||
|  | ||||
| # reCAPTCHA | ||||
| # recaptcha: | ||||
| #   site_key: example-site-key | ||||
| #  secret_key: example-secret-key | ||||
|  | ||||
| # ServiceWorker | ||||
| # sw: | ||||
| #   # Public key of VAPID | ||||
| #   public_key: example-sw-public-key | ||||
|  | ||||
| #   # Private key of VAPID | ||||
| #   private_key: example-sw-private-key | ||||
|  | ||||
| # google_maps_api_key: example-google-maps-api-key | ||||
|  | ||||
| # Twitter integration | ||||
| # twitter: | ||||
| #   consumer_key: example-twitter-consumer-key | ||||
| #   consumer_secret: example-twitter-consumer-secret-key | ||||
|  | ||||
| # Ghost | ||||
| # Ghost account is an account used for the purpose of delegating | ||||
| # followers when putting users in the list. | ||||
| # ghost: user-id-of-your-ghost-account | ||||
|  | ||||
| # Clustering | ||||
| # clusterLimit: 1 | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -2,3 +2,4 @@ | ||||
| *.psd -diff -text | ||||
| *.ai -diff -text | ||||
| yarn.lock -diff -text | ||||
| package-lock.json -diff -text | ||||
|   | ||||
							
								
								
									
										7
									
								
								.github/ISSUE_TEMPLATE
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,7 +0,0 @@ | ||||
| <!-- | ||||
| Misskeyへの貢献ありがとうございます。 | ||||
|  | ||||
| バグの報告や提案などで、可能であれば以下の情報を含めてください。 | ||||
| * お使いのブラウザ | ||||
| * デスクトップ版Misskeyかモバイル版Misskeyか | ||||
| --> | ||||
							
								
								
									
										22
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | ||||
| --- | ||||
| name: Bug Report | ||||
| about: Create a report to help us improve | ||||
| --- | ||||
|  | ||||
| # Summary | ||||
| <!-- Tell us what the bug is --> | ||||
|  | ||||
| # Expected Behavior | ||||
| <!--- Tell us what should happen --> | ||||
|  | ||||
| # Actual Behavior | ||||
| <!--- Tell us what happens instead of the expected behavior --> | ||||
|  | ||||
| # Steps to Reproduce | ||||
| 1. | ||||
| 2. | ||||
| 3. | ||||
|  | ||||
| # Environment | ||||
| <!-- Tell us where on the platform it happens --> | ||||
| <!-- e.g. desktop or mobile version, your browser, your OS --> | ||||
							
								
								
									
										11
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| --- | ||||
| name: Feature Request | ||||
| about: Suggest an idea for this project | ||||
| --- | ||||
|  | ||||
| # Summary | ||||
| <!-- Tell us what the suggestion is --> | ||||
|  | ||||
| # Environment | ||||
| <!-- Tell us where on the platform it related --> | ||||
| <!-- e.g. desktop or mobile version, your browser, your OS --> | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -10,5 +10,4 @@ npm-debug.log | ||||
| *.pem | ||||
| run.bat | ||||
| api-docs.json | ||||
| package-lock.json | ||||
| *.log | ||||
|   | ||||
							
								
								
									
										12
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| { | ||||
| 	"recommendations": [ | ||||
| 		"ducksoupdev.vue2", | ||||
| 		"editorconfig.editorconfig", | ||||
| 		"eg2.tslint", | ||||
| 		"eg2.vscode-npm-script", | ||||
| 		"hollowtree.vue-snippets", | ||||
| 		"ms-vscode.typescript-javascript-grammar", | ||||
| 		"octref.vetur", | ||||
| 		"sysoev.language-stylus" | ||||
| 	] | ||||
| } | ||||
| @@ -5,6 +5,15 @@ ChangeLog | ||||
|  | ||||
| This document describes breaking changes only. | ||||
|  | ||||
| 5.0.0 | ||||
| ----- | ||||
|  | ||||
| ### Migration | ||||
|  | ||||
| 起動する前に、`node cli/migration/5.0.0`してください。 | ||||
|  | ||||
| Please run `node cli/migration/5.0.0` before launch. | ||||
|  | ||||
| 4.0.0 | ||||
| ----- | ||||
|  | ||||
|   | ||||
							
								
								
									
										23
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -7,7 +7,7 @@ | ||||
| [![][dependencies-badge]][dependencies-link] | ||||
| [](http://makeapullrequest.com) [](https://greenkeeper.io/) | ||||
|  | ||||
| > Lead Maintainer: [syuilo][syuilo-link] | ||||
| **Microblogging. Redefined.** | ||||
|  | ||||
| **[Misskey](https://misskey.xyz)** is a completely open source, | ||||
| ultimately sophisticated professional microblogging software. | ||||
| @@ -18,14 +18,13 @@ ultimately sophisticated professional microblogging software. | ||||
|  | ||||
| :sparkles: Features | ||||
| ---------------------------------------------------------------- | ||||
| * Rich text contents | ||||
| * Reactions | ||||
| * User lists | ||||
| * Customizable column view (known as MisskeyDeck) | ||||
| * Customizable column view (called MisskeyDeck) | ||||
|   * and widgets! | ||||
| * Private messages | ||||
| * Mute | ||||
| * Real-time timelines | ||||
| * ActivityPub compatible | ||||
| * ActivityPub support | ||||
|  | ||||
| and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz). | ||||
|  | ||||
| @@ -44,15 +43,15 @@ If you want to... | ||||
|  | ||||
| :heart: Backers & Sponsors | ||||
| ---------------------------------------------------------------- | ||||
| | ![][nagarus-icon] | ![][dansup-icon] | | ||||
| |:-:|:-:| | ||||
| | [nagarus][nagarus-link] | [dansup][dansup-link] | | ||||
| | <img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=tB1e_r8RlZ5sFL0KV_e8dugapxatNBRK1Z3h67TO1g8%3D"> | <img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12378075/0156f769e20f412594fa6b87d85fe228/1?token-time=2145916800&token-hash=IsIJRUXszzoD6-7pDnRY8I05T9nSznc4GTaxj7C9SwU%3D"> | <img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=S1zP0QyLU52Dqq6dtc9qNYyWfW86XrYHiR4NMbeOrnA%3D"> | <img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D"> | | ||||
| |:-:|:-:|:-:|:-:| | ||||
| | [Gargron](https://www.patreon.com/mastodon) | [39ff](https://www.patreon.com/user/creators?u=12378075) | [dansup](https://www.patreon.com/dansup) | [Takashi Shibuya](https://www.patreon.com/user/creators?u=12531784) | | ||||
|  | ||||
| :four_leaf_clover: Copyright | ||||
| ---------------------------------------------------------------- | ||||
| > Copyright (c) 2014-2018 syuilo | ||||
|  | ||||
| Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE). | ||||
| Misskey is an open-source software licensed under the [GNU AGPLv3](LICENSE). | ||||
|  | ||||
| [![][agpl-3.0-badge]][AGPL-3.0] | ||||
|  | ||||
| @@ -73,9 +72,3 @@ Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE). | ||||
|  | ||||
| [syuilo-link]:      https://syuilo.com | ||||
| [syuilo-icon]:      https://avatars2.githubusercontent.com/u/4439005?v=3&s=70 | ||||
|  | ||||
| [nagarus-link]: https://www.patreon.com/user/creators?u=11601413 | ||||
| [nagarus-icon]: https://c10.patreonusercontent.com/3/eyJ2IjoiMSIsInciOjIwMH0%3D/patreon-media/user/11601413/20cb15f209924302b399b99d3c98b850?token-time=2145916800&token-hash=IO31nK6VZCMWBWU2VAk2c824BX2QZ4DNPKyHHZXS0iw%3D | ||||
| [dansup-link]: https://www.patreon.com/dansup | ||||
| [dansup-icon]: https://c10.patreonusercontent.com/3/eyJ2IjoiMSIsInciOjIwMH0%3D/patreon-media/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb?token-time=2145916800&token-hash=opXAM_pnhUTuN1jCA6p_Nn_YsaqohY465YFjWFqMEEE%3D | ||||
|  | ||||
|   | ||||
							
								
								
									
										41
									
								
								appveyor.yml
									
									
									
									
									
								
							
							
						
						| @@ -1,41 +0,0 @@ | ||||
| # appveyor file | ||||
| # http://www.appveyor.com/docs/appveyor-yml | ||||
|  | ||||
| environment: | ||||
|   matrix: | ||||
|     - nodejs_version: 10.1.0 | ||||
|  | ||||
| cache: | ||||
|   - node_modules | ||||
|  | ||||
| build: off | ||||
|  | ||||
| install: | ||||
|   # Update Node.js | ||||
|   # 標準で入っている Node.js を更新します (2014/11/13 時点では、v0.10.32 が標準) | ||||
|   - ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) | ||||
|   - node --version | ||||
|  | ||||
|   # Update NPM | ||||
|   - npm install -g npm | ||||
|   - npm --version | ||||
|  | ||||
|   # Update node-gyp | ||||
|   # 必須! node-gyp のバージョンを上げないと、ネイティブモジュールのコンパイルに失敗します | ||||
|   - npm install -g node-gyp | ||||
|  | ||||
|   - npm install | ||||
|  | ||||
| init: | ||||
|   # git clone の際の改行を変換しないようにします | ||||
|   - git config --global core.autocrlf false | ||||
|  | ||||
| before_test: | ||||
|   # 設定ファイルを配置 | ||||
|   - cp ./.travis/default.yml ./.config | ||||
|   - cp ./.travis/test.yml ./.config | ||||
|  | ||||
|   - npm run build | ||||
|  | ||||
| test_script: | ||||
|   - npm test | ||||
| Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 2.5 KiB | 
| Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 3.9 KiB | 
| Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.5 KiB | 
| Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.8 KiB | 
| Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 2.5 KiB | 
| Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/title.png
									
									
									
									
									
								
							
							
						
						| Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 3.8 KiB | 
| @@ -9,7 +9,7 @@ const q = { | ||||
| 	'metadata._user.host': { | ||||
| 		$ne: null | ||||
| 	}, | ||||
| 	'metadata.isMetaOnly': false | ||||
| 	'metadata.withoutChunks': false | ||||
| }; | ||||
|  | ||||
| async function main() { | ||||
| @@ -57,7 +57,7 @@ async function main() { | ||||
|  | ||||
| 					DriveFile.update({ _id: file._id }, { | ||||
| 						$set: { | ||||
| 							'metadata.isMetaOnly': true | ||||
| 							'metadata.withoutChunks': true | ||||
| 						} | ||||
| 					}) | ||||
| 				]).then(async () => { | ||||
|   | ||||
							
								
								
									
										168
									
								
								cli/init.js
									
									
									
									
									
								
							
							
						
						| @@ -1,168 +0,0 @@ | ||||
| const fs = require('fs'); | ||||
| const path = require('path'); | ||||
| const yaml = require('js-yaml'); | ||||
| const inquirer = require('inquirer'); | ||||
| const chalk = require('chalk'); | ||||
|  | ||||
| const configDirPath = `${__dirname}/../.config`; | ||||
| const configPath = `${configDirPath}/default.yml`; | ||||
|  | ||||
| const form = [{ | ||||
| 	type: 'input', | ||||
| 	name: 'maintainerName', | ||||
| 	message: 'Your name:' | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'maintainerUrl', | ||||
| 	message: 'Your home page URL or your mailto URL:' | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'url', | ||||
| 	message: 'URL you want to run Misskey:', | ||||
| 	validate: function(wannabeurl) { | ||||
| 		return wannabeurl.match('^http\(s?\)://') ? true : | ||||
| 		       'URL needs to start with http:// or https://'; | ||||
| 	} | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'port', | ||||
| 	message: 'Listen port (e.g. 443):' | ||||
| }, { | ||||
| 	type: 'confirm', | ||||
| 	name: 'https', | ||||
| 	message: 'Use TLS?', | ||||
| 	default: false | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'https_key', | ||||
| 	message: 'Path of tls key:', | ||||
| 	when: ctx => ctx.https | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'https_cert', | ||||
| 	message: 'Path of tls cert:', | ||||
| 	when: ctx => ctx.https | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'https_ca', | ||||
| 	message: 'Path of tls ca:', | ||||
| 	when: ctx => ctx.https | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'mongo_host', | ||||
| 	message: 'MongoDB\'s host:', | ||||
| 	default: 'localhost' | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'mongo_port', | ||||
| 	message: 'MongoDB\'s port:', | ||||
| 	default: '27017' | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'mongo_db', | ||||
| 	message: 'MongoDB\'s db:', | ||||
| 	default: 'misskey' | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'mongo_user', | ||||
| 	message: 'MongoDB\'s user:' | ||||
| }, { | ||||
| 	type: 'password', | ||||
| 	name: 'mongo_pass', | ||||
| 	message: 'MongoDB\'s password:' | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'redis_host', | ||||
| 	message: 'Redis\'s host:', | ||||
| 	default: 'localhost' | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'redis_port', | ||||
| 	message: 'Redis\'s port:', | ||||
| 	default: '6379' | ||||
| }, { | ||||
| 	type: 'password', | ||||
| 	name: 'redis_pass', | ||||
| 	message: 'Redis\'s password:' | ||||
| }, { | ||||
| 	type: 'confirm', | ||||
| 	name: 'elasticsearch', | ||||
| 	message: 'Use Elasticsearch?', | ||||
| 	default: false | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'es_host', | ||||
| 	message: 'Elasticsearch\'s host:', | ||||
| 	default: 'localhost', | ||||
| 	when: ctx => ctx.elasticsearch | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'es_port', | ||||
| 	message: 'Elasticsearch\'s port:', | ||||
| 	default: '9200', | ||||
| 	when: ctx => ctx.elasticsearch | ||||
| }, { | ||||
| 	type: 'password', | ||||
| 	name: 'es_pass', | ||||
| 	message: 'Elasticsearch\'s password:', | ||||
| 	when: ctx => ctx.elasticsearch | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'recaptcha_site', | ||||
| 	message: 'reCAPTCHA\'s site key:' | ||||
| }, { | ||||
| 	type: 'input', | ||||
| 	name: 'recaptcha_secret', | ||||
| 	message: 'reCAPTCHA\'s secret key:' | ||||
| }]; | ||||
|  | ||||
| inquirer.prompt(form).then(as => { | ||||
| 	// Mapping answers | ||||
| 	const conf = { | ||||
| 		maintainer: { | ||||
| 			name: as['maintainerName'], | ||||
| 			url: as['maintainerUrl'] | ||||
| 		}, | ||||
| 		url: as['url'], | ||||
| 		port: parseInt(as['port'], 10), | ||||
| 		mongodb: { | ||||
| 			host: as['mongo_host'], | ||||
| 			port: parseInt(as['mongo_port'], 10), | ||||
| 			db: as['mongo_db'], | ||||
| 			user: as['mongo_user'], | ||||
| 			pass: as['mongo_pass'] | ||||
| 		}, | ||||
| 		redis: { | ||||
| 			host: as['redis_host'], | ||||
| 			port: parseInt(as['redis_port'], 10), | ||||
| 			pass: as['redis_pass'] | ||||
| 		}, | ||||
| 		elasticsearch: { | ||||
| 			enable: as['elasticsearch'], | ||||
| 			host: as['es_host'] || null, | ||||
| 			port: parseInt(as['es_port'], 10) || null, | ||||
| 			pass: as['es_pass'] || null | ||||
| 		}, | ||||
| 		recaptcha: { | ||||
| 			site_key: as['recaptcha_site'], | ||||
| 			secret_key: as['recaptcha_secret'] | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	if (as['https']) { | ||||
| 		conf.https = { | ||||
| 			key: as['https_key'] || null, | ||||
| 			cert: as['https_cert'] || null, | ||||
| 			ca: as['https_ca'] || null | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	console.log(`Thanks. Writing the configuration to ${chalk.bold(path.resolve(configPath))}`); | ||||
|  | ||||
| 	try { | ||||
| 		fs.writeFileSync(configPath, yaml.dump(conf)); | ||||
| 		console.log(chalk.green('Well done.')); | ||||
| 	} catch (e) { | ||||
| 		console.error(e); | ||||
| 	} | ||||
| }); | ||||
							
								
								
									
										23
									
								
								cli/mark-admin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | ||||
| const mongo = require('mongodb'); | ||||
| const User = require('../built/models/user').default; | ||||
|  | ||||
| const args = process.argv.slice(2); | ||||
|  | ||||
| const user = args[0]; | ||||
|  | ||||
| const q = user.startsWith('@') ? { | ||||
| 	username: user.split('@')[1], | ||||
| 	host: user.split('@')[2] || null | ||||
| } : { _id: new mongo.ObjectID(user) }; | ||||
|  | ||||
| console.log(`Mark as admin ${user}...`); | ||||
|  | ||||
| User.update(q, { | ||||
| 	$set: { | ||||
| 		isAdmin: true | ||||
| 	} | ||||
| }).then(() => { | ||||
| 	console.log(`Done ${user}`); | ||||
| }, e => { | ||||
| 	console.error(e); | ||||
| }); | ||||
							
								
								
									
										23
									
								
								cli/mark-verified.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | ||||
| const mongo = require('mongodb'); | ||||
| const User = require('../built/models/user').default; | ||||
|  | ||||
| const args = process.argv.slice(2); | ||||
|  | ||||
| const user = args[0]; | ||||
|  | ||||
| const q = user.startsWith('@') ? { | ||||
| 	username: user.split('@')[1], | ||||
| 	host: user.split('@')[2] || null | ||||
| } : { _id: new mongo.ObjectID(user) }; | ||||
|  | ||||
| console.log(`Mark as verfied ${user}...`); | ||||
|  | ||||
| User.update(q, { | ||||
| 	$set: { | ||||
| 		isVerified: true | ||||
| 	} | ||||
| }).then(() => { | ||||
| 	console.log(`Done ${user}`); | ||||
| }, e => { | ||||
| 	console.error(e); | ||||
| }); | ||||
| @@ -3,8 +3,8 @@ | ||||
| const chalk = require('chalk'); | ||||
| const sequential = require('promise-sequential'); | ||||
| 
 | ||||
| const { default: User } = require('../built/models/user'); | ||||
| const { default: DriveFile } = require('../built/models/drive-file'); | ||||
| const { default: User } = require('../../built/models/user'); | ||||
| const { default: DriveFile } = require('../../built/models/drive-file'); | ||||
| 
 | ||||
| async function main() { | ||||
| 	const promiseGens = []; | ||||
| @@ -3,8 +3,8 @@ | ||||
| const chalk = require('chalk'); | ||||
| const sequential = require('promise-sequential'); | ||||
| 
 | ||||
| const { default: User } = require('../built/models/user'); | ||||
| const { default: DriveFile } = require('../built/models/drive-file'); | ||||
| const { default: User } = require('../../built/models/user'); | ||||
| const { default: DriveFile } = require('../../built/models/drive-file'); | ||||
| 
 | ||||
| async function main() { | ||||
| 	const promiseGens = []; | ||||
							
								
								
									
										9
									
								
								cli/migration/5.0.0.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| const { default: DriveFile } = require('../../built/models/drive-file'); | ||||
|  | ||||
| DriveFile.update({}, { | ||||
| 	$rename: { | ||||
| 		'metadata.isMetaOnly': 'metadata.withoutChunks' | ||||
| 	} | ||||
| }, { | ||||
| 	multi: true | ||||
| }); | ||||
							
								
								
									
										29
									
								
								cli/reset-password.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,29 @@ | ||||
| const mongo = require('mongodb'); | ||||
| const bcrypt = require('bcryptjs'); | ||||
| const User = require('../built/models/user').default; | ||||
|  | ||||
| const args = process.argv.slice(2); | ||||
|  | ||||
| const user = args[0]; | ||||
|  | ||||
| const q = user.startsWith('@') ? { | ||||
| 	username: user.split('@')[1], | ||||
| 	host: user.split('@')[2] || null | ||||
| } : { _id: new mongo.ObjectID(user) }; | ||||
|  | ||||
| console.log(`Resetting password for ${user}...`); | ||||
|  | ||||
| const passwd = 'yo'; | ||||
|  | ||||
| // Generate hash of password | ||||
| const hash = bcrypt.hashSync(passwd); | ||||
|  | ||||
| User.update(q, { | ||||
| 	$set: { | ||||
| 		password: hash | ||||
| 	} | ||||
| }).then(() => { | ||||
| 	console.log(`Password of ${user} is now '${passwd}'`); | ||||
| }, e => { | ||||
| 	console.error(e); | ||||
| }); | ||||
| @@ -14,7 +14,7 @@ RUN pacman -S --noconfirm pacman | ||||
| RUN pacman-db-upgrade | ||||
| RUN pacman -S --noconfirm archlinux-keyring | ||||
| RUN pacman -Syyu --noconfirm | ||||
| RUN pacman -S --noconfirm git nodejs npm mongodb redis imagemagick | ||||
| RUN pacman -S --noconfirm git nodejs npm mongodb redis | ||||
|  | ||||
| COPY misskey.sh /root/misskey.sh | ||||
| RUN chmod u+x /root/misskey.sh | ||||
|   | ||||
							
								
								
									
										6
									
								
								docs/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| # Docs | ||||
| These docs are for contributors of Misskey or admins of instance of Misskey. | ||||
| Docs for users are located in `src/docs`. | ||||
|  | ||||
| これらのドキュメントはMisskeyの開発者またはMisskeyインスタンス運営者向けです。 | ||||
| 利用者向けのドキュメントは`src/docs`にあります。 | ||||
							
								
								
									
										46
									
								
								docs/manage.en.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,46 @@ | ||||
| # Management guide | ||||
|  | ||||
| ## Check the status of the job queue | ||||
| coming soon | ||||
|  | ||||
| ## Mark as 'admin' user | ||||
| ``` shell | ||||
| node cli/mark-admin (User-ID or Username) | ||||
| ``` | ||||
|  | ||||
| ## Mark as 'verified' user | ||||
| ``` shell | ||||
| node cli/mark-verified (User-ID or Username) | ||||
| ``` | ||||
|  | ||||
| ## Suspend users | ||||
| ``` shell | ||||
| node cli/suspend (User-ID or Username) | ||||
| ``` | ||||
| e.g. | ||||
| ``` shell | ||||
| # Use id | ||||
| node cli/suspend 57d01a501fdf2d07be417afe | ||||
|  | ||||
| # Use username | ||||
| node cli/suspend @syuilo | ||||
|  | ||||
| # Use username (remote) | ||||
| node cli/suspend @syuilo@misskey.xyz | ||||
| ``` | ||||
|  | ||||
| ## Reset password | ||||
| ``` shell | ||||
| node cli/reset-password (User-ID or Username) | ||||
| ``` | ||||
|  | ||||
| ## Clean up cached remote files | ||||
| ``` shell | ||||
| node cli/clean-cached-remote-files | ||||
| ``` | ||||
|  | ||||
| ## Clean up unused drive files | ||||
| ``` shell | ||||
| node cli/clean-unused-drive-files | ||||
| ``` | ||||
| > We recommend that you announce a user that unused drive files will be deleted before performing this operation, as it may delete the user's important files. | ||||
| @@ -1,13 +1,46 @@ | ||||
| # 運営ガイド | ||||
|  | ||||
| ## ジョブキューの状態を調べる | ||||
| Misskeyのディレクトリで: | ||||
| coming soon | ||||
|  | ||||
| ## 管理者ユーザーを設定する | ||||
| ``` shell | ||||
| node_modules/kue/bin/kue-dashboard -p 3050 | ||||
| node cli/mark-admin (ユーザーID または ユーザー名) | ||||
| ``` | ||||
|  | ||||
| ## 'verified'ユーザーを設定する | ||||
| ``` shell | ||||
| node cli/mark-verified (ユーザーID または ユーザー名) | ||||
| ``` | ||||
| ポート3050にアクセスするとUIが表示されます | ||||
|  | ||||
| ## ユーザーを凍結する | ||||
| ``` shell | ||||
| node cli/suspend (ユーザーID) | ||||
| node cli/suspend (ユーザーID または ユーザー名) | ||||
| ``` | ||||
| 例: | ||||
| ``` shell | ||||
| # ユーザーID | ||||
| node cli/suspend 57d01a501fdf2d07be417afe | ||||
|  | ||||
| # ユーザー名 | ||||
| node cli/suspend @syuilo | ||||
|  | ||||
| # ユーザー名 (リモート) | ||||
| node cli/suspend @syuilo@misskey.xyz | ||||
| ``` | ||||
|  | ||||
| ## ユーザーのパスワードをリセットする | ||||
| ``` shell | ||||
| node cli/reset-password (ユーザーID または ユーザー名) | ||||
| ``` | ||||
|  | ||||
| ## キャッシュされたリモートファイルをクリーンアップする | ||||
| ``` shell | ||||
| node cli/clean-cached-remote-files | ||||
| ``` | ||||
|  | ||||
| ## 使われていないドライブのファイルをクリーンアップする | ||||
| ``` shell | ||||
| node cli/clean-unused-drive-files | ||||
| ``` | ||||
| > ユーザーの大事なファイルを削除する可能性があるので、この操作を実行する前にユーザーに告知することをお勧めします。 | ||||
|   | ||||
							
								
								
									
										110
									
								
								docs/setup.en.md
									
									
									
									
									
								
							
							
						
						| @@ -8,18 +8,13 @@ This guide describes how to install and setup Misskey. | ||||
|  | ||||
| ---------------------------------------------------------------- | ||||
|  | ||||
| *1.* reCAPTCHA tokens | ||||
| *1.* Create Misskey user | ||||
| ---------------------------------------------------------------- | ||||
| Misskey requires reCAPTCHA tokens. | ||||
| Please visit https://www.google.com/recaptcha/intro/ and generate keys. | ||||
| Running misskey on root is not a good idea so we create a user for that. | ||||
| In debian for exemple : | ||||
|  | ||||
| *(optional)* Generating VAPID keys | ||||
| ---------------------------------------------------------------- | ||||
| If you want to enable ServiceWroker, you need to generate VAPID keys: | ||||
|  | ||||
| ``` shell | ||||
| npm install web-push -g | ||||
| web-push generate-vapid-keys | ||||
| ``` | ||||
| adduser --disabled-password --disabled-login misskey | ||||
| ``` | ||||
|  | ||||
| *2.* Install dependencies | ||||
| @@ -27,25 +22,52 @@ web-push generate-vapid-keys | ||||
| Please install and setup these softwares: | ||||
|  | ||||
| #### Dependencies :package: | ||||
| * *Node.js* and *npm* | ||||
| * **[MongoDB](https://www.mongodb.com/)** | ||||
| * **[Node.js](https://nodejs.org/en/)** | ||||
| * **[MongoDB](https://www.mongodb.com/)** >= 3.6 | ||||
| * **[Redis](https://redis.io/)** | ||||
| * **[ImageMagick](http://www.imagemagick.org/script/index.php)** >= 7.0 | ||||
|  | ||||
| ##### Optional | ||||
| * [Elasticsearch](https://www.elastic.co/) - used to provide searching feature instead of MongoDB | ||||
|  | ||||
| *3.* Install Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 1. `git clone -b master git://github.com/syuilo/misskey.git` | ||||
| 2. `cd misskey` | ||||
| 3. `npm install` | ||||
|  | ||||
| *4.* Prepare configuration | ||||
| *3.* Setup MongoDB | ||||
| ---------------------------------------------------------------- | ||||
| You need to generate config file via `npm run config` command. | ||||
| In root : | ||||
| 1. `mongo` Go to the mongo shell | ||||
| 2. `use misskey` Use the misskey database | ||||
| 3. `db.users.save( {dummy:"dummy"} )` Write dummy data to initialize the db. | ||||
| 4. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` Create the misskey user. | ||||
| 5. `exit` You're done ! | ||||
|  | ||||
| *5.* Build Misskey | ||||
| *4.* Install Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 1. `su - misskey` Connect to misskey user. | ||||
| 2. `git clone -b master git://github.com/syuilo/misskey.git` Clone the misskey repo from master branch. | ||||
| 3. `cd misskey` Navigate to misskey directory | ||||
| 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)* reCAPTCHA tokens | ||||
| ---------------------------------------------------------------- | ||||
| If you want to enable reCAPTCHA, you need to generate reCAPTCHA tokens: | ||||
| Please visit https://www.google.com/recaptcha/intro/ and generate keys. | ||||
|  | ||||
| *(optional)* Generating VAPID keys | ||||
| ---------------------------------------------------------------- | ||||
| If you want to enable ServiceWroker, you need to generate VAPID keys: | ||||
| Unless you have set your global node_modules location elsewhere, you need to run this in root. | ||||
|  | ||||
| ``` shell | ||||
| npm install web-push -g | ||||
| web-push generate-vapid-keys | ||||
| ``` | ||||
|  | ||||
| *5.* Make configuration file | ||||
| ---------------------------------------------------------------- | ||||
| 1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`. | ||||
| 2. Edit `default.yml` | ||||
|  | ||||
| *6.* Build Misskey | ||||
| ---------------------------------------------------------------- | ||||
|  | ||||
| Build misskey with the following: | ||||
| @@ -61,14 +83,48 @@ If you're still encountering errors about some modules, use node-gyp: | ||||
| 3. `node-gyp build` | ||||
| 4. `npm run build` | ||||
|  | ||||
| *6.* That is it. | ||||
| *7.* That is it. | ||||
| ---------------------------------------------------------------- | ||||
| Well done! Now, you have an environment that run to Misskey. | ||||
|  | ||||
| ### Launch | ||||
| Just `sudo npm start`. GLHF! | ||||
| ### Launch normally | ||||
| Just `npm start`. GLHF! | ||||
|  | ||||
| ### Launch with systemd | ||||
|  | ||||
| 1. Create a systemd service here: `/etc/systemd/system/misskey.service` | ||||
| 2. Edit it, and paste this and save: | ||||
|  | ||||
| ``` | ||||
| [Unit] | ||||
| Description=Misskey daemon | ||||
|  | ||||
| [Service] | ||||
| Type=simple | ||||
| User=misskey | ||||
| ExecStart=/usr/bin/npm start | ||||
| WorkingDirectory=/home/misskey/misskey | ||||
| TimeoutSec=60 | ||||
| StandardOutput=syslog | ||||
| StandardError=syslog | ||||
| SyslogIdentifier=misskey | ||||
| Restart=always | ||||
|  | ||||
| [Install] | ||||
| WantedBy=multi-user.target | ||||
| ``` | ||||
|  | ||||
| 3. `systemctl daemon-reload ; systemctl enable misskey` Reload systemd and enable the misskey service. | ||||
| 4. `systemctl start misskey` Start the misskey service. | ||||
|  | ||||
| You can check if the service is running with `systemctl status misskey`. | ||||
|  | ||||
| ### Way to Update to latest version of your Misskey | ||||
| 1. `git reset --hard && git pull origin master` | ||||
| 2. `npm install` | ||||
| 3. `npm run build` | ||||
| 1. `git fetch` | ||||
| 2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` | ||||
| 3. `npm install` | ||||
| 4. `npm run build` | ||||
|  | ||||
| ---------------------------------------------------------------- | ||||
|  | ||||
| If you have any questions or troubles, feel free to contact us! | ||||
|   | ||||
							
								
								
									
										127
									
								
								docs/setup.ja.md
									
									
									
									
									
								
							
							
						
						| @@ -8,10 +8,48 @@ Misskeyサーバーの構築にご関心をお寄せいただきありがとう | ||||
|  | ||||
| ---------------------------------------------------------------- | ||||
|  | ||||
| *1.* reCAPTCHAトークンの用意 | ||||
| *1.* Misskeyユーザーの作成 | ||||
| ---------------------------------------------------------------- | ||||
| MisskeyはreCAPTCHAトークンを必要とします。 | ||||
| https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生成してください。 | ||||
| Misskeyのrootで実行しない方がよいため、代わりにユーザーを作成します。 | ||||
| Debianの例: | ||||
|  | ||||
| ``` | ||||
| adduser --disabled-password --disabled-login misskey | ||||
| ``` | ||||
|  | ||||
| *2.* 依存関係をインストールする | ||||
| ---------------------------------------------------------------- | ||||
| これらのソフトウェアをインストール・設定してください: | ||||
|  | ||||
| #### 依存関係 :package: | ||||
| * **[Node.js](https://nodejs.org/en/)** | ||||
| * **[MongoDB](https://www.mongodb.com/)** (3.6以上) | ||||
| * **[Redis](https://redis.io/)** | ||||
|  | ||||
| ##### オプション | ||||
| * [Elasticsearch](https://www.elastic.co/) - 検索機能を向上させるために用います。 | ||||
|  | ||||
| *3.* MongoDBの設定 | ||||
| ---------------------------------------------------------------- | ||||
| ルートで: | ||||
| 1. `mongo` mongoシェルを起動 | ||||
| 2. `use misskey` misskeyデータベースを使用 | ||||
| 3. `db.users.save( {dummy:"dummy"} )` ダミーデータを書き込みDBを初期化 | ||||
| 4. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` misskeyユーザーを作成 | ||||
| 5. `exit` mongoシェルを終了 | ||||
|  | ||||
| *4.* Misskeyのインストール | ||||
| ---------------------------------------------------------------- | ||||
| 1. `su - misskey` misskeyユーザーを使用 | ||||
| 2. `git clone -b master git://github.com/syuilo/misskey.git` masterブランチからMisskeyレポジトリをクローン | ||||
| 3. `cd misskey` 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の依存パッケージをインストール | ||||
|  | ||||
| *(オプション)* reCAPTCHAトークン | ||||
| ---------------------------------------------------------------- | ||||
| reCAPTCHAを有効にする場合、reCAPTCHAトークンを取得する必要があります。 | ||||
| https://www.google.com/recaptcha/intro/ にアクセスしてトークンを取得してください。 | ||||
|  | ||||
| *(オプション)* VAPIDキーペアの生成 | ||||
| ---------------------------------------------------------------- | ||||
| @@ -22,56 +60,67 @@ npm install web-push -g | ||||
| web-push generate-vapid-keys | ||||
| ``` | ||||
|  | ||||
| *2.* 依存関係をインストールする | ||||
| *5.* 設定ファイルを作成する | ||||
| ---------------------------------------------------------------- | ||||
| これらのソフトウェアをインストール・設定してください: | ||||
| 1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする。 | ||||
| 2. `default.yml` を編集する。 | ||||
|  | ||||
| #### 依存関係 :package: | ||||
| * *Node.js* と *npm* | ||||
| * **[MongoDB](https://www.mongodb.com/)** | ||||
| * **[Redis](https://redis.io/)** | ||||
| * **[ImageMagick](http://www.imagemagick.org/script/index.php)** | ||||
|  | ||||
| ##### オプション | ||||
| * [Elasticsearch](https://www.elastic.co/) - 検索機能を向上させるために用います。 | ||||
|  | ||||
| *3.* Misskeyのインストール | ||||
| *6.* Misskeyのビルド | ||||
| ---------------------------------------------------------------- | ||||
| 1. `git clone -b master git://github.com/syuilo/misskey.git` | ||||
| 2. `cd misskey` | ||||
| 3. `npm install` | ||||
|  | ||||
| *4.* 設定ファイルを用意する | ||||
| ---------------------------------------------------------------- | ||||
| `npm run config`コマンドを利用して、ガイドに従って情報を入力してください。 | ||||
| 次のコマンドでMisskeyをビルドしてください: | ||||
|  | ||||
| *5.* Misskeyのビルド | ||||
| ---------------------------------------------------------------- | ||||
| `npm run build` | ||||
|  | ||||
| Debianをお使いであれば、`build-essential`パッケージをインストールする必要があります。 | ||||
|  | ||||
| 何らかのモジュールでエラーが発生する場合はnode-gypを使ってください: | ||||
| 1. `npm install -g node-gyp` | ||||
| 2. `node-gyp configure` | ||||
| 3. `node-gyp build` | ||||
| 4. `npm run build` | ||||
|  | ||||
| *6.* 以上です! | ||||
| *7.* 以上です! | ||||
| ---------------------------------------------------------------- | ||||
| お疲れ様でした。これでMisskeyを動かす準備は整いました。 | ||||
|  | ||||
| ### 起動 | ||||
| `sudo npm start`するだけです。GLHF! | ||||
| ### 通常起動 | ||||
| `npm start`するだけです。GLHF! | ||||
|  | ||||
| ### systemdを用いた起動 | ||||
| 1. systemdサービスのファイルを作成: `/etc/systemd/system/misskey.service` | ||||
| 2. エディタで開き、以下のコードを貼り付けて保存: | ||||
|  | ||||
| ``` | ||||
| [Unit] | ||||
| Description=Misskey daemon | ||||
|  | ||||
| [Service] | ||||
| Type=simple | ||||
| User=misskey | ||||
| ExecStart=/usr/bin/npm start | ||||
| WorkingDirectory=/home/misskey/misskey | ||||
| TimeoutSec=60 | ||||
| StandardOutput=syslog | ||||
| StandardError=syslog | ||||
| SyslogIdentifier=misskey | ||||
| Restart=always | ||||
|  | ||||
| [Install] | ||||
| WantedBy=multi-user.target | ||||
| ``` | ||||
|  | ||||
| 3. `systemctl daemon-reload ; systemctl enable misskey` systemdを再読み込みしmisskeyサービスを有効化 | ||||
| 4. `systemctl start misskey` misskeyサービスの起動 | ||||
|  | ||||
| `systemctl status misskey`と入力すると、サービスの状態を調べることができます。 | ||||
|  | ||||
| ### Misskeyを最新バージョンにアップデートする方法: | ||||
| 1. `git reset --hard && git pull origin master` | ||||
| 2. `npm install` | ||||
| 3. `npm run build` | ||||
| 1. `git fetch` | ||||
| 2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` | ||||
| 3. `npm install` | ||||
| 4. `npm run build` | ||||
|  | ||||
| ## メモリが足りなくてビルドできない場合 | ||||
| Misskeyの(クライアントの)ビルドには、目安として8GBくらいのメモリを必要とします。 | ||||
| VPSなどでビルドする時は、もしかしたらメモリが足りなくなる可能性があります。 | ||||
| そうなった場合、もしVPSではなくあなたのPCが十分なメモリを搭載しているなら、あなたのPC上でビルドし、生成されたファイルをVPSにFTPでアップロードする方法を採ることができます。 | ||||
| ---------------------------------------------------------------- | ||||
|  | ||||
| 1. あなたのPC上にMisskeyをインストールする | ||||
| 2. 設定ファイルを用意する。設定ファイルは、サーバーに合わせた設定にします。 | ||||
| 3. npm run webpack | ||||
| 4. built/client をサーバーにアップロードする | ||||
| 5. サーバー上で、npm run gulp | ||||
| 6. 完了 | ||||
| なにかお困りのことがありましたらお気軽にご連絡ください。 | ||||
|   | ||||
| @@ -4,19 +4,19 @@ Misskey's Translation | ||||
| If you find an untranslated part on Misskey: | ||||
| -------------------------------------------- | ||||
|  | ||||
| 1. Look for untranslated parts in the miskey's source code. | ||||
| 1. Look for untranslated parts in the misskey's source code. | ||||
| 	- For instance, if you find an untranslated part in: `src/client/app/mobile/views/pages/home.vue`. | ||||
|  | ||||
| 2. Replace the untranslated portion with a character string of the form `%i18n:@foo%`. | ||||
| 	- In fact, `foo` should be a word that is appropriate for the situation and is easy to understand in English. | ||||
| 	- For example, if the untranslated portion is the following "タイムライン" you must write: `%i18n:@timeline%`. | ||||
|  | ||||
| 3. Open each language file in /locales, check whether the <strong>file name (path)</strong> found in step 1 exists, if not, create it. | ||||
| 3. Open the `locales/ja.yml`, check whether the <strong>file name (path)</strong> found in step 1 exists, if not, create it. | ||||
| 	- Do not put the beginning of the path `src/client/app/` in the locale file. | ||||
| 	- For example, in this case we want to modify untranslated parts of `src/client/app/mobile/views/pages/home.vue`, so the key is `mobile/views/pages/home.vue`. | ||||
|  | ||||
| 4. Add the translated text property using the `foo` keyword below the path that you found or created in step 2. Make sure to type your text in quotation marks. Text should always be inside of quotes. | ||||
| 	-   For example, in this case we add timeline: `timeline: "Timeline"` to `locales/en.yml`, and `timeline: "タイムライン"` to `locales/ja.yml`. | ||||
| 4. Add the text property using the `foo` keyword below the path that you found or created in step 2. Make sure to type your text in quotation marks. Text should always be inside of quotes. | ||||
| 	-   For example, in this case we add timeline: `timeline: "タイムライン"` to `locales/ja.yml`. | ||||
|  | ||||
| 5. And done! | ||||
|  | ||||
|   | ||||
| @@ -11,12 +11,12 @@ Misskey内の未翻訳箇所を見つけたら | ||||
| 	- `foo`は実際にはその場に適したわかりやすい(英語の)名前にしてください。 | ||||
| 	- 例えば未翻訳箇所が「タイムライン」というテキストだった場合、`%i18n:@timeline%`のようにします。 | ||||
|  | ||||
| 3. /locales 内にあるそれぞれの言語ファイルを開き、1.で見つけた<strong>ファイル名(パス)</strong>のキーが存在するか確認し、無ければ作成してください。 | ||||
| 3. `locales/ja.yml`を開き、1.で見つけた<strong>ファイル名(パス)</strong>のキーが存在するか確認し、無ければ作成してください。 | ||||
| 	- パスの`src/client/app/`は省略してください。 | ||||
| 	- 例えば、今回の例では`src/client/app/mobile/views/pages/home.vue`の未翻訳箇所を修正したいので、キーは`mobile/views/pages/home.vue`になります。 | ||||
|  | ||||
| 4. そのキーの直下に2.で置換した`foo`の部分をキーとし、翻訳後のテキストを値とするプロパティを追加します。 | ||||
| 	- 例えば、今回の例で言うと`locales/ja.yml`に`timeline: "タイムライン"`、`locales/en.yml`に`timeline: "Timeline"`を追加します。 | ||||
| 4. そのキーの直下に2.で置換した`foo`の部分をキーとし、テキストを値とするプロパティを追加します。 | ||||
| 	- 例えば、今回の例で言うと`locales/ja.yml`に`timeline: "タイムライン"`を追加します。 | ||||
|  | ||||
| 5. 完了です! | ||||
|  | ||||
|   | ||||
| @@ -1,6 +0,0 @@ | ||||
| How to create indexes | ||||
| ===================== | ||||
|  | ||||
| ``` shell | ||||
| curl -XPOST localhost:9200/misskey -d @path/to/mappings.json | ||||
| ``` | ||||
| @@ -1,65 +0,0 @@ | ||||
| { | ||||
| 	"settings": { | ||||
| 		"analysis": { | ||||
| 			"analyzer": { | ||||
| 				"bigram": { | ||||
| 					"tokenizer": "bigram_tokenizer" | ||||
| 				} | ||||
| 			}, | ||||
| 			"tokenizer": { | ||||
| 				"bigram_tokenizer": { | ||||
| 					"type": "nGram", | ||||
| 					"min_gram": 2, | ||||
| 					"max_gram": 2, | ||||
| 					"token_chars": [ | ||||
| 						"letter", | ||||
| 						"digit" | ||||
| 					] | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 	"mappings": { | ||||
| 		"user": { | ||||
| 			"properties": { | ||||
| 				"username": { | ||||
| 					"type": "string", | ||||
| 					"index": "analyzed", | ||||
| 					"analyzer": "bigram" | ||||
| 				}, | ||||
| 				"name": { | ||||
| 					"type": "string", | ||||
| 					"index": "analyzed", | ||||
| 					"analyzer": "bigram" | ||||
| 				}, | ||||
| 				"bio": { | ||||
| 					"type": "string", | ||||
| 					"index": "analyzed", | ||||
| 					"analyzer": "kuromoji" | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		"post": { | ||||
| 			"properties": { | ||||
| 				"text": { | ||||
| 					"type": "string", | ||||
| 					"index": "analyzed", | ||||
| 					"analyzer": "kuromoji" | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		"drive_file": { | ||||
| 			"properties": { | ||||
| 				"name": { | ||||
| 					"type": "string", | ||||
| 					"index": "analyzed", | ||||
| 					"analyzer": "kuromoji" | ||||
| 				}, | ||||
| 				"user": { | ||||
| 					"type": "string", | ||||
| 					"index": "not_analyzed" | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										42
									
								
								gulpfile.ts
									
									
									
									
									
								
							
							
						
						| @@ -9,6 +9,7 @@ import * as ts from 'gulp-typescript'; | ||||
| const sourcemaps = require('gulp-sourcemaps'); | ||||
| import tslint from 'gulp-tslint'; | ||||
| const cssnano = require('gulp-cssnano'); | ||||
| const stylus = require('gulp-stylus'); | ||||
| import * as uglifyComposer from 'gulp-uglify/composer'; | ||||
| import pug = require('gulp-pug'); | ||||
| import * as rimraf from 'rimraf'; | ||||
| @@ -20,9 +21,8 @@ import * as replace from 'gulp-replace'; | ||||
| import * as htmlmin from 'gulp-htmlmin'; | ||||
| const uglifyes = require('uglify-es'); | ||||
|  | ||||
| import locales from './locales'; | ||||
| import { fa } from './src/build/fa'; | ||||
| const client = require('./built/client/meta.json'); | ||||
| const locales = require('./locales'); | ||||
| import { fa } from './src/misc/fa'; | ||||
| import config from './src/config'; | ||||
|  | ||||
| const uglify = uglifyComposer(uglifyes, console); | ||||
| @@ -38,8 +38,6 @@ if (isDebug) { | ||||
|  | ||||
| const constants = require('./src/const.json'); | ||||
|  | ||||
| require('./src/client/docs/gulpfile.ts'); | ||||
|  | ||||
| gulp.task('build', [ | ||||
| 	'build:ts', | ||||
| 	'build:copy', | ||||
| @@ -47,8 +45,6 @@ gulp.task('build', [ | ||||
| 	'doc' | ||||
| ]); | ||||
|  | ||||
| gulp.task('rebuild', ['clean', 'build']); | ||||
|  | ||||
| gulp.task('build:ts', () => { | ||||
| 	const tsProject = ts.createProject('./tsconfig.json'); | ||||
|  | ||||
| @@ -85,19 +81,19 @@ gulp.task('lint', () => | ||||
| ); | ||||
|  | ||||
| gulp.task('format', () => | ||||
| gulp.src('./src/**/*.ts') | ||||
| 	.pipe(tslint({ | ||||
| 		formatter: 'verbose', | ||||
| 		fix: true | ||||
| 	})) | ||||
| 	.pipe(tslint.report()) | ||||
| 	gulp.src('./src/**/*.ts') | ||||
| 		.pipe(tslint({ | ||||
| 			formatter: 'verbose', | ||||
| 			fix: true | ||||
| 		})) | ||||
| 		.pipe(tslint.report()) | ||||
| ); | ||||
|  | ||||
| gulp.task('mocha', () => | ||||
| 	gulp.src([]) | ||||
| 	gulp.src('./test/**/*.ts') | ||||
| 		.pipe(mocha({ | ||||
| 			exit: true, | ||||
| 			compilers: 'ts:ts-node/register' | ||||
| 			require: 'ts-node/register' | ||||
| 		} as any)) | ||||
| ); | ||||
|  | ||||
| @@ -118,8 +114,9 @@ gulp.task('build:client', [ | ||||
| 	'copy:client' | ||||
| ]); | ||||
|  | ||||
| gulp.task('build:client:script', () => | ||||
| 	gulp.src(['./src/client/app/boot.js', './src/client/app/safe.js']) | ||||
| gulp.task('build:client:script', () => { | ||||
| 	const client = require('./built/client/meta.json'); | ||||
| 	return gulp.src(['./src/client/app/boot.js', './src/client/app/safe.js']) | ||||
| 		.pipe(replace('VERSION', JSON.stringify(client.version))) | ||||
| 		.pipe(replace('API', JSON.stringify(config.api_url))) | ||||
| 		.pipe(replace('ENV', JSON.stringify(env))) | ||||
| @@ -127,8 +124,8 @@ gulp.task('build:client:script', () => | ||||
| 		.pipe(isProduction ? uglify({ | ||||
| 			toplevel: true | ||||
| 		} as any) : gutil.noop()) | ||||
| 		.pipe(gulp.dest('./built/client/assets/')) as any | ||||
| ); | ||||
| 		.pipe(gulp.dest('./built/client/assets/')); | ||||
| }); | ||||
|  | ||||
| gulp.task('build:client:styles', () => | ||||
| 	gulp.src('./src/client/app/init.css') | ||||
| @@ -201,3 +198,10 @@ gulp.task('build:client:pug', [ | ||||
| 			})) | ||||
| 			.pipe(gulp.dest('./built/client/app/')) | ||||
| ); | ||||
|  | ||||
| gulp.task('doc', () => | ||||
| 	gulp.src('./src/docs/**/*.styl') | ||||
| 		.pipe(stylus()) | ||||
| 		.pipe((cssnano as any)()) | ||||
| 		.pipe(gulp.dest('./built/docs/assets/')) | ||||
| ); | ||||
|   | ||||
| @@ -889,6 +889,7 @@ mobile/views/pages/settings/settings.profile.vue: | ||||
|   saved: "Profile updated" | ||||
|   uploading: "Uploading" | ||||
|   upload-failed: "Failed to upload" | ||||
|    | ||||
| mobile/views/pages/search.vue: | ||||
|   search: "Search" | ||||
|   empty: "No posts were found for '{}'" | ||||
|   | ||||
| @@ -40,7 +40,7 @@ common: | ||||
|     hmm: "Hmm ... ?" | ||||
|     surprise: "Wow" | ||||
|     congrats: "Félicitations !" | ||||
|     angry: "En colère" | ||||
|     angry: "Faché" | ||||
|     confused: "Confus" | ||||
|     pudding: "Pudding" | ||||
|   note-placeholders: | ||||
|   | ||||
							
								
								
									
										27
									
								
								locales/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| /** | ||||
|  * Languages Loader | ||||
|  */ | ||||
|  | ||||
| const fs = require('fs'); | ||||
| const yaml = require('js-yaml'); | ||||
|  | ||||
| const loadLang = lang => yaml.safeLoad( | ||||
| 	fs.readFileSync(`${__dirname}/${lang}.yml`, 'utf-8')); | ||||
|  | ||||
| const native = loadLang('ja'); | ||||
|  | ||||
| const langs = { | ||||
| 	'de': loadLang('de'), | ||||
| 	'en': loadLang('en'), | ||||
| 	'fr': loadLang('fr'), | ||||
| 	'ja': native, | ||||
| 	'pl': loadLang('pl'), | ||||
| 	'es': loadLang('es') | ||||
| }; | ||||
|  | ||||
| Object.values(langs).forEach(locale => { | ||||
| 	// Extend native language (Japanese) | ||||
| 	locale = Object.assign({}, native, locale); | ||||
| }); | ||||
|  | ||||
| module.exports = langs; | ||||
| @@ -1,34 +0,0 @@ | ||||
| /** | ||||
|  * Languages Loader | ||||
|  */ | ||||
|  | ||||
| import * as fs from 'fs'; | ||||
| import * as yaml from 'js-yaml'; | ||||
|  | ||||
| export type LangKey = 'de' | 'en' | 'fr' | 'ja' | 'pl' | 'es'; | ||||
| export type LocaleObject = { [key: string]: any }; | ||||
|  | ||||
| const loadLang = (lang: LangKey) => yaml.safeLoad( | ||||
| 	fs.readFileSync(`./locales/${lang}.yml`, 'utf-8')) as LocaleObject; | ||||
|  | ||||
| const native = loadLang('ja'); | ||||
|  | ||||
| const langs: { [key: string]: LocaleObject } = { | ||||
| 	'de': loadLang('de'), | ||||
| 	'en': loadLang('en'), | ||||
| 	'fr': loadLang('fr'), | ||||
| 	'ja': native, | ||||
| 	'pl': loadLang('pl'), | ||||
| 	'es': loadLang('es') | ||||
| }; | ||||
|  | ||||
| Object.entries(langs).map(([, locale]) => { | ||||
| 	// Extend native language (Japanese) | ||||
| 	locale = Object.assign({}, native, locale); | ||||
| }); | ||||
|  | ||||
| export function isAvailableLanguage(lang: string): lang is LangKey { | ||||
| 	return lang in langs; | ||||
| } | ||||
|  | ||||
| export default langs; | ||||
| @@ -6,6 +6,14 @@ common: | ||||
|   misskey: "A ⭐ of fediverse" | ||||
|   about-title: "A ⭐ of fediverse." | ||||
|   about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。" | ||||
|    | ||||
|   customization-tips: | ||||
|     title: "カスタマイズのヒント" | ||||
|     paragraph1: "ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。" | ||||
|     paragraph2: "一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。" | ||||
|     paragraph3: "ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。" | ||||
|     paragraph4: "カスタマイズを終了するには、右上の「完了」をクリックします。" | ||||
|     gotit: "Got it!" | ||||
|  | ||||
|   time: | ||||
|     unknown: "なぞのじかん" | ||||
| @@ -19,6 +27,8 @@ common: | ||||
|     months_ago: "{}ヶ月前" | ||||
|     years_ago: "{}年前" | ||||
|  | ||||
|   trash: "ゴミ箱" | ||||
|  | ||||
|   weekday-short: | ||||
|     sunday: "日" | ||||
|     monday: "月" | ||||
| @@ -56,6 +66,7 @@ common: | ||||
|   my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。" | ||||
|   i-like-sushi: "私は(プリンよりむしろ)寿司が好き" | ||||
|   show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示" | ||||
|   verified-user: "認証済みのユーザー" | ||||
|  | ||||
|   reversi: | ||||
|     drawn: "引き分け" | ||||
| @@ -63,6 +74,7 @@ common: | ||||
|     opponent-turn: "相手のターンです" | ||||
|     turn-of: "{}のターンです" | ||||
|     past-turn-of: "{}のターン" | ||||
|     won: "{}の勝ち" | ||||
|  | ||||
|   widgets: | ||||
|     analog-clock: "アナログ時計" | ||||
| @@ -93,6 +105,7 @@ common: | ||||
|     widgets: "ウィジェット" | ||||
|     home: "ホーム" | ||||
|     local: "ローカル" | ||||
|     hybrid: "ソーシャル" | ||||
|     global: "グローバル" | ||||
|     notifications: "通知" | ||||
|     list: "リスト" | ||||
| @@ -279,6 +292,11 @@ common/views/widgets/memo.vue: | ||||
|   title: "付箋" | ||||
|   memo: "ここに書いて!" | ||||
|   save: "保存" | ||||
|    | ||||
| common/views/widgets/slideshow.vue: | ||||
|   folder-customize-mode: "フォルダを指定するには、カスタマイズモードを終了してください" | ||||
|   folder: "クリックしてフォルダを指定してください" | ||||
|   no-image: "このフォルダには画像がありません" | ||||
|  | ||||
| common/views/pages/follow.vue: | ||||
|   signed-in-as: "{}としてサインイン中" | ||||
| @@ -329,6 +347,8 @@ desktop/views/components/drive.file.vue: | ||||
|   banner: "バナー" | ||||
|   contextmenu: | ||||
|     rename: "名前を変更" | ||||
|     mark-as-sensitive: "閲覧注意に設定" | ||||
|     unmark-as-sensitive: "閲覧注意を解除" | ||||
|     copy-url: "URLをコピー" | ||||
|     download: "ダウンロード" | ||||
|     else-files: "その他..." | ||||
| @@ -376,6 +396,14 @@ desktop/views/components/drive.vue: | ||||
|     upload: "ファイルをアップロード" | ||||
|     url-upload: "URLからアップロード" | ||||
|  | ||||
| desktop/views/components/media-image.vue: | ||||
|   sensitive: "閲覧注意" | ||||
|   click-to-show: "クリックして表示" | ||||
|  | ||||
| desktop/views/components/media-video.vue: | ||||
|   sensitive: "閲覧注意" | ||||
|   click-to-show: "クリックして表示" | ||||
|  | ||||
| desktop/views/components/follow-button.vue: | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
| @@ -440,12 +468,16 @@ desktop/views/components/notes.note.vue: | ||||
| desktop/views/components/notes.vue: | ||||
|   error: "読み込みに失敗しました。" | ||||
|   retry: "リトライ" | ||||
|   load-more: "もっと読み込む" | ||||
|  | ||||
| desktop/views/components/notifications.vue: | ||||
|   more: "もっと見る" | ||||
|   empty: "ありません!" | ||||
|  | ||||
| desktop/views/components/post-form.vue: | ||||
|   add-visible-user: "+ユーザーを追加" | ||||
|   attach-location-information: "位置情報を添付する" | ||||
|   hide-contents: "内容を隠す" | ||||
|   reply-placeholder: "この投稿への返信..." | ||||
|   quote-placeholder: "この投稿を引用..." | ||||
|   submit: "投稿" | ||||
| @@ -464,7 +496,13 @@ desktop/views/components/post-form.vue: | ||||
|   insert-a-kao: "v('ω')v" | ||||
|   create-poll: "アンケートを作成" | ||||
|   text-remain: "残り{}文字" | ||||
|  | ||||
|   recent-tags: "最近" | ||||
|   click-to-tagging: "クリックでタグ付け" | ||||
|   visibility: "公開範囲" | ||||
|   geolocation-alert: "お使いの端末は位置情報に対応していません" | ||||
|   error: "エラー" | ||||
|   enter-username: "ユーザー名を入力してください" | ||||
|    | ||||
| desktop/views/components/post-form-window.vue: | ||||
|   note: "新規投稿" | ||||
|   reply: "返信" | ||||
| @@ -512,6 +550,8 @@ desktop/views/components/settings.vue: | ||||
|  | ||||
|   display: "デザインと表示" | ||||
|   customize: "ホームをカスタマイズ" | ||||
|   choose-wallpaper: "壁紙を選択" | ||||
|   delete-wallpaper: "壁紙を削除" | ||||
|   dark-mode: "ダークモード" | ||||
|   circle-icons: "円形のアイコンを使用" | ||||
|   gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用" | ||||
| @@ -621,8 +661,12 @@ desktop/views/components/settings.profile.vue: | ||||
|   description: "自己紹介" | ||||
|   birthday: "誕生日" | ||||
|   save: "保存" | ||||
|   locked-account: "アカウントの保護" | ||||
|   is-locked: "投稿を非公開にする" | ||||
|   other: "その他" | ||||
|   is-bot: "このアカウントはBotです" | ||||
|   is-cat: "このアカウントはCatです" | ||||
|   profile-updated: "プロフィールを更新しました" | ||||
|  | ||||
| desktop/views/components/sub-note-content.vue: | ||||
|   private: "この投稿は非公開です" | ||||
| @@ -636,6 +680,7 @@ desktop/views/components/taskmanager.vue: | ||||
| desktop/views/components/timeline.vue: | ||||
|   home: "ホーム" | ||||
|   local: "ローカル" | ||||
|   hybrid: "ソーシャル" | ||||
|   global: "グローバル" | ||||
|   list: "リスト" | ||||
|  | ||||
| @@ -648,7 +693,7 @@ desktop/views/components/ui.header.account.vue: | ||||
|   favorites: "お気に入り" | ||||
|   lists: "リスト" | ||||
|   follow-requests: "フォロー申請" | ||||
|   customize: "カスタマイズ" | ||||
|   customize: "ホームのカスタマイズ" | ||||
|   settings: "設定" | ||||
|   signout: "サインアウト" | ||||
|   dark: "闇に飲まれる" | ||||
| @@ -698,6 +743,7 @@ desktop/views/components/window.vue: | ||||
| desktop/views/pages/deck/deck.tl-column.vue: | ||||
|   is-media-only: "メディア投稿のみ" | ||||
|   is-media-view: "メディアビュー" | ||||
|   edit: "オプション" | ||||
|  | ||||
| desktop/views/pages/deck/deck.note.vue: | ||||
|   reposted-by: "{}がRenote" | ||||
| @@ -844,6 +890,14 @@ mobile/views/components/drive.file-detail.vue: | ||||
|   hash: "ハッシュ (md5)" | ||||
|   exif: "EXIF" | ||||
|  | ||||
| mobile/views/components/media-image.vue: | ||||
|   sensitive: "閲覧注意" | ||||
|   click-to-show: "クリックして表示" | ||||
|  | ||||
| mobile/views/components/media-video.vue: | ||||
|   sensitive: "閲覧注意" | ||||
|   click-to-show: "クリックして表示" | ||||
|  | ||||
| mobile/views/components/follow-button.vue: | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
| @@ -958,6 +1012,7 @@ mobile/views/pages/following.vue: | ||||
| mobile/views/pages/home.vue: | ||||
|   home: "ホーム" | ||||
|   local: "ローカル" | ||||
|   hybrid: "ソーシャル" | ||||
|   global: "グローバル" | ||||
|  | ||||
| mobile/views/pages/messaging.vue: | ||||
| @@ -1088,11 +1143,17 @@ docs: | ||||
|       properties: "プロパティ" | ||||
|     endpoints: | ||||
|       params: "パラメータ" | ||||
|       no-params: "パラメータはありません" | ||||
|       res: "レスポンス" | ||||
|       require-credential: "このエンドポイントは認証情報が必須です。" | ||||
|       require-permission: "このエンドポイントは{permission}の権限を必要とします。" | ||||
|       has-limit: "レートリミットがあります。" | ||||
|       duration-limit: "直近{duration}ミリ秒の間のこのエンドポイントへのリクエスト数の合計が{max}を超える場合はリクエストできません。" | ||||
|       min-interval-limit: "前回のリクエストから{interval}ミリ秒経っていない場合はリクエストできません。" | ||||
|       show-src: "このエンドポイントのソースコードも閲覧できます。" | ||||
|       show-src-link: "コードをGitHubで見る" | ||||
|       generated: "このドキュメントはAPI定義に基づき自動生成されています。" | ||||
|     props: | ||||
|       name: "名前" | ||||
|       type: "型" | ||||
|       optional: "オプション" | ||||
|       description: "説明" | ||||
|       yes: "はい" | ||||
|       no: "いいえ" | ||||
|   | ||||
| @@ -1,11 +0,0 @@ | ||||
| Misskeyの破壊的変更に対応するいくつかのスニペットがあります。 | ||||
| MongoDBシェルで実行する必要のあるものとnodeで直接実行する必要のあるものがあります。 | ||||
| ファイル名が `shell.` から始まるものは前者、 `node.` から始まるものは後者です。 | ||||
|  | ||||
| MongoDBシェルで実行する場合、`use`でデータベースを選択しておく必要があります。 | ||||
|  | ||||
| nodeで実行するいくつかのスニペットは、並列処理させる数を引数で設定できるものがあります。 | ||||
| 処理中にエラーで落ちる場合は、メモリが足りていない可能性があるので、少ない数に設定してみてください。 | ||||
| ※デフォルトは`5`です。 | ||||
|  | ||||
| ファイルを作成する際は `../init-migration-file.sh -t _type_ -n _name_` を実行すると _type_._unixtime_._name_.js が生成されます | ||||
| @@ -1,37 +0,0 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| usage() { | ||||
| 		echo "$0 [-t type] [-n name]" | ||||
| 		echo "  type: [node | shell]" | ||||
| 		echo "  name: if no present, set untitled" | ||||
| 		exit 0 | ||||
| } | ||||
|  | ||||
| while getopts :t:n:h OPT | ||||
| do | ||||
| 	case $OPT in | ||||
| 		t)	type=$OPTARG | ||||
| 				;; | ||||
| 		n)	name=$OPTARG | ||||
| 				;; | ||||
| 		h)	usage | ||||
| 				;; | ||||
| 		\?) usage | ||||
| 				;; | ||||
| 		:)	usage | ||||
| 				;; | ||||
| 	esac | ||||
| done | ||||
|  | ||||
| if [ "$type" = "" ] | ||||
| then | ||||
| 	echo "no type present!!!" | ||||
| 	usage | ||||
| fi | ||||
|  | ||||
| if [ "$name" = "" ] | ||||
| then | ||||
| 	name="untitled" | ||||
| fi | ||||
|  | ||||
| touch "$(realpath $(dirname $BASH_SOURCE))/migration/$type.$(date +%s).$name.js" | ||||
							
								
								
									
										100
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,21 +1,18 @@ | ||||
| { | ||||
| 	"name": "misskey", | ||||
| 	"author": "syuilo <i@syuilo.com>", | ||||
| 	"version": "4.15.0", | ||||
| 	"clientVersion": "1.0.6878", | ||||
| 	"version": "5.8.0", | ||||
| 	"clientVersion": "1.0.7664", | ||||
| 	"codename": "nighthike", | ||||
| 	"main": "./built/index.js", | ||||
| 	"private": true, | ||||
| 	"scripts": { | ||||
| 		"config": "node ./cli/init.js", | ||||
| 		"start": "node ./built", | ||||
| 		"debug": "DEBUG=misskey:* node ./built", | ||||
| 		"swagger": "node ./swagger.js", | ||||
| 		"build": "webpack && gulp build", | ||||
| 		"webpack": "webpack", | ||||
| 		"watch": "webpack --watch", | ||||
| 		"gulp": "gulp build", | ||||
| 		"rebuild": "gulp rebuild", | ||||
| 		"clean": "gulp clean", | ||||
| 		"cleanall": "gulp cleanall", | ||||
| 		"lint": "gulp lint", | ||||
| @@ -27,15 +24,15 @@ | ||||
| 		"@fortawesome/fontawesome-free-brands": "5.0.13", | ||||
| 		"@fortawesome/fontawesome-free-regular": "5.0.13", | ||||
| 		"@fortawesome/fontawesome-free-solid": "5.0.13", | ||||
| 		"@koa/cors": "2.2.1", | ||||
| 		"@koa/cors": "2.2.2", | ||||
| 		"@prezzemolo/rap": "0.1.2", | ||||
| 		"@prezzemolo/zip": "0.0.3", | ||||
| 		"@types/bcryptjs": "2.4.1", | ||||
| 		"@types/dateformat": "1.0.1", | ||||
| 		"@types/debug": "0.0.30", | ||||
| 		"@types/deep-equal": "1.0.1", | ||||
| 		"@types/elasticsearch": "5.0.24", | ||||
| 		"@types/elasticsearch": "5.0.25", | ||||
| 		"@types/file-type": "5.2.1", | ||||
| 		"@types/gm": "1.18.0", | ||||
| 		"@types/gulp": "3.8.36", | ||||
| 		"@types/gulp-htmlmin": "1.3.32", | ||||
| 		"@types/gulp-mocha": "0.0.32", | ||||
| @@ -43,31 +40,29 @@ | ||||
| 		"@types/gulp-replace": "0.0.31", | ||||
| 		"@types/gulp-uglify": "3.0.5", | ||||
| 		"@types/gulp-util": "3.0.34", | ||||
| 		"@types/inquirer": "0.0.42", | ||||
| 		"@types/is-root": "1.0.0", | ||||
| 		"@types/is-url": "1.2.28", | ||||
| 		"@types/js-yaml": "3.11.1", | ||||
| 		"@types/js-yaml": "3.11.2", | ||||
| 		"@types/jsdom": "11.0.6", | ||||
| 		"@types/koa": "2.0.46", | ||||
| 		"@types/koa-bodyparser": "5.0.0", | ||||
| 		"@types/koa-bodyparser": "5.0.1", | ||||
| 		"@types/koa-compress": "2.0.8", | ||||
| 		"@types/koa-favicon": "2.0.19", | ||||
| 		"@types/koa-logger": "3.1.0", | ||||
| 		"@types/koa-mount": "3.0.1", | ||||
| 		"@types/koa-multer": "1.0.0", | ||||
| 		"@types/koa-router": "7.0.30", | ||||
| 		"@types/koa-router": "7.0.31", | ||||
| 		"@types/koa-send": "4.1.1", | ||||
| 		"@types/koa-views": "2.0.3", | ||||
| 		"@types/koa__cors": "2.2.2", | ||||
| 		"@types/kue": "0.11.9", | ||||
| 		"@types/license-checker": "15.0.0", | ||||
| 		"@types/koa__cors": "2.2.3", | ||||
| 		"@types/minio": "6.0.2", | ||||
| 		"@types/mkdirp": "0.5.2", | ||||
| 		"@types/mocha": "5.2.3", | ||||
| 		"@types/mongodb": "3.1.0", | ||||
| 		"@types/mongodb": "3.1.2", | ||||
| 		"@types/ms": "0.7.30", | ||||
| 		"@types/node": "10.5.1", | ||||
| 		"@types/nopt": "3.0.29", | ||||
| 		"@types/node": "10.5.4", | ||||
| 		"@types/parse5": "5.0.0", | ||||
| 		"@types/portscanner": "2.1.0", | ||||
| 		"@types/pug": "2.0.4", | ||||
| 		"@types/qrcode": "1.2.0", | ||||
| 		"@types/ratelimiter": "2.1.28", | ||||
| @@ -76,11 +71,14 @@ | ||||
| 		"@types/request-promise-native": "1.0.15", | ||||
| 		"@types/rimraf": "2.0.2", | ||||
| 		"@types/seedrandom": "2.4.27", | ||||
| 		"@types/sharp": "0.17.9", | ||||
| 		"@types/showdown": "1.7.5", | ||||
| 		"@types/single-line-log": "1.1.0", | ||||
| 		"@types/speakeasy": "2.0.2", | ||||
| 		"@types/systeminformation": "3.23.0", | ||||
| 		"@types/tmp": "0.0.33", | ||||
| 		"@types/uuid": "3.4.3", | ||||
| 		"@types/webpack": "4.4.4", | ||||
| 		"@types/webpack": "4.4.8", | ||||
| 		"@types/webpack-stream": "3.2.10", | ||||
| 		"@types/websocket": "0.0.39", | ||||
| 		"@types/ws": "5.1.2", | ||||
| @@ -88,51 +86,54 @@ | ||||
| 		"autosize": "4.0.2", | ||||
| 		"autwh": "0.1.0", | ||||
| 		"bcryptjs": "2.4.3", | ||||
| 		"bee-queue": "1.2.2", | ||||
| 		"bootstrap-vue": "2.0.0-rc.11", | ||||
| 		"cafy": "8.0.0", | ||||
| 		"cafy": "11.3.0", | ||||
| 		"chalk": "2.4.1", | ||||
| 		"commander": "2.16.0", | ||||
| 		"crc-32": "1.2.0", | ||||
| 		"css-loader": "0.28.11", | ||||
| 		"css-loader": "1.0.0", | ||||
| 		"dateformat": "3.0.3", | ||||
| 		"debug": "3.1.0", | ||||
| 		"deep-equal": "1.0.1", | ||||
| 		"deepcopy": "0.6.3", | ||||
| 		"diskusage": "0.2.4", | ||||
| 		"dompurify": "1.0.5", | ||||
| 		"elasticsearch": "15.0.0", | ||||
| 		"element-ui": "2.4.2", | ||||
| 		"emojilib": "2.2.12", | ||||
| 		"elasticsearch": "15.1.1", | ||||
| 		"element-ui": "2.4.5", | ||||
| 		"emojilib": "2.3.0", | ||||
| 		"escape-regexp": "0.0.1", | ||||
| 		"eslint": "5.0.1", | ||||
| 		"eslint-plugin-vue": "4.5.0", | ||||
| 		"eslint-plugin-vue": "4.7.1", | ||||
| 		"eventemitter3": "3.1.0", | ||||
| 		"exif-js": "2.3.0", | ||||
| 		"file-loader": "1.1.11", | ||||
| 		"file-type": "8.0.0", | ||||
| 		"file-type": "8.1.0", | ||||
| 		"fuckadblock": "3.2.1", | ||||
| 		"gm": "1.23.1", | ||||
| 		"gulp": "3.9.1", | ||||
| 		"gulp-cssnano": "2.1.3", | ||||
| 		"gulp-htmlmin": "4.0.0", | ||||
| 		"gulp-imagemin": "4.1.0", | ||||
| 		"gulp-mocha": "6.0.0", | ||||
| 		"gulp-pug": "4.0.1", | ||||
| 		"gulp-rename": "1.3.0", | ||||
| 		"gulp-rename": "1.4.0", | ||||
| 		"gulp-replace": "1.0.0", | ||||
| 		"gulp-sourcemaps": "2.6.4", | ||||
| 		"gulp-stylus": "2.7.0", | ||||
| 		"gulp-tslint": "8.1.3", | ||||
| 		"gulp-typescript": "4.0.2", | ||||
| 		"gulp-uglify": "3.0.0", | ||||
| 		"gulp-uglify": "3.0.1", | ||||
| 		"gulp-util": "3.0.8", | ||||
| 		"hard-source-webpack-plugin": "0.10.1", | ||||
| 		"hard-source-webpack-plugin": "0.12.0", | ||||
| 		"highlight.js": "9.12.0", | ||||
| 		"html-minifier": "3.5.17", | ||||
| 		"html-minifier": "3.5.19", | ||||
| 		"http-signature": "1.2.0", | ||||
| 		"inquirer": "6.0.0", | ||||
| 		"insert-text-at-cursor": "0.1.1", | ||||
| 		"is-root": "2.0.0", | ||||
| 		"is-url": "1.2.4", | ||||
| 		"jquery": "3.3.1", | ||||
| 		"js-yaml": "3.12.0", | ||||
| 		"jsdom": "11.11.0", | ||||
| 		"jsdom": "11.12.0", | ||||
| 		"koa": "2.5.1", | ||||
| 		"koa-bodyparser": "4.2.1", | ||||
| 		"koa-compress": "3.0.0", | ||||
| @@ -145,32 +146,30 @@ | ||||
| 		"koa-send": "5.0.0", | ||||
| 		"koa-slow": "2.1.0", | ||||
| 		"koa-views": "6.1.4", | ||||
| 		"kue": "0.11.6", | ||||
| 		"license-checker": "20.1.0", | ||||
| 		"loader-utils": "1.1.0", | ||||
| 		"mecab-async": "0.1.2", | ||||
| 		"minio": "6.0.0", | ||||
| 		"mkdirp": "0.5.1", | ||||
| 		"mocha": "5.2.0", | ||||
| 		"moji": "0.5.1", | ||||
| 		"mongodb": "3.1.0", | ||||
| 		"mongodb": "3.1.1", | ||||
| 		"monk": "6.0.6", | ||||
| 		"ms": "2.1.1", | ||||
| 		"nan": "2.10.0", | ||||
| 		"node-sass": "4.9.0", | ||||
| 		"node-sass": "4.9.2", | ||||
| 		"node-sass-json-importer": "3.3.1", | ||||
| 		"nopt": "4.0.1", | ||||
| 		"nprogress": "0.2.0", | ||||
| 		"object-assign-deep": "0.4.0", | ||||
| 		"on-build-webpack": "0.1.0", | ||||
| 		"os-utils": "0.0.14", | ||||
| 		"parse5": "5.0.0", | ||||
| 		"portscanner": "2.2.0", | ||||
| 		"progress-bar-webpack-plugin": "1.11.0", | ||||
| 		"prominence": "0.2.0", | ||||
| 		"promise-sequential": "1.1.1", | ||||
| 		"pug": "2.0.3", | ||||
| 		"punycode": "2.1.1", | ||||
| 		"qrcode": "1.2.0", | ||||
| 		"ratelimiter": "3.1.0", | ||||
| 		"qrcode": "1.2.2", | ||||
| 		"ratelimiter": "3.2.0", | ||||
| 		"recaptcha-promise": "0.1.3", | ||||
| 		"reconnecting-websocket": "3.2.2", | ||||
| 		"redis": "2.8.0", | ||||
| @@ -181,22 +180,24 @@ | ||||
| 		"s-age": "1.1.2", | ||||
| 		"sass-loader": "7.0.3", | ||||
| 		"seedrandom": "2.4.3", | ||||
| 		"sharp": "0.20.5", | ||||
| 		"showdown": "1.8.6", | ||||
| 		"showdown-highlightjs-extension": "0.1.2", | ||||
| 		"single-line-log": "1.1.2", | ||||
| 		"speakeasy": "2.0.0", | ||||
| 		"style-loader": "0.21.0", | ||||
| 		"stylus": "0.54.5", | ||||
| 		"stylus-loader": "3.0.2", | ||||
| 		"summaly": "2.0.6", | ||||
| 		"swagger-jsdoc": "1.9.7", | ||||
| 		"systeminformation": "3.42.4", | ||||
| 		"syuilo-password-strength": "0.0.1", | ||||
| 		"tcp-port-used": "0.1.2", | ||||
| 		"textarea-caret": "3.1.0", | ||||
| 		"tmp": "0.0.33", | ||||
| 		"ts-loader": "4.4.1", | ||||
| 		"ts-node": "7.0.0", | ||||
| 		"tslint": "5.10.0", | ||||
| 		"typescript": "2.9.2", | ||||
| 		"typescript-eslint-parser": "16.0.1", | ||||
| 		"typescript-eslint-parser": "17.0.1", | ||||
| 		"uglify-es": "3.3.9", | ||||
| 		"url-loader": "1.0.1", | ||||
| 		"uuid": "3.3.2", | ||||
| @@ -205,18 +206,19 @@ | ||||
| 		"vue-cropperjs": "2.2.1", | ||||
| 		"vue-js-modal": "1.3.16", | ||||
| 		"vue-json-tree-view": "2.1.4", | ||||
| 		"vue-loader": "15.2.4", | ||||
| 		"vue-loader": "15.2.6", | ||||
| 		"vue-router": "3.0.1", | ||||
| 		"vue-style-loader": "4.1.1", | ||||
| 		"vue-template-compiler": "2.5.16", | ||||
| 		"vuedraggable": "2.16.0", | ||||
| 		"vuex": "3.0.1", | ||||
| 		"vuex-persistedstate": "^2.5.4", | ||||
| 		"vuex-persistedstate": "2.5.4", | ||||
| 		"web-push": "3.3.2", | ||||
| 		"webfinger.js": "2.6.6", | ||||
| 		"webpack": "4.14.0", | ||||
| 		"webpack-cli": "3.0.8", | ||||
| 		"webpack": "4.16.3", | ||||
| 		"webpack-cli": "3.1.0", | ||||
| 		"websocket": "1.0.26", | ||||
| 		"ws": "5.2.1", | ||||
| 		"ws": "6.0.0", | ||||
| 		"xev": "2.0.1" | ||||
| 	}, | ||||
| 	"greenkeeper": { | ||||
|   | ||||
| @@ -1,5 +0,0 @@ | ||||
| import { IUser } from '../models/user'; | ||||
|  | ||||
| export default (user: IUser) => { | ||||
| 	return user.host === null ? user.username : `${user.username}@${user.host}`; | ||||
| }; | ||||
| @@ -2,7 +2,7 @@ | ||||
| <div class="form"> | ||||
| 	<header> | ||||
| 		<h1><i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか?</h1> | ||||
| 		<img :src="`${app.iconUrl}?thumbnail&size=64`"/> | ||||
| 		<img :src="app.iconUrl"/> | ||||
| 	</header> | ||||
| 	<div class="app"> | ||||
| 		<section> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import getNoteSummary from '../../../../renderers/get-note-summary'; | ||||
| import getReactionEmoji from '../../../../renderers/get-reaction-emoji'; | ||||
| import getUserName from '../../../../renderers/get-user-name'; | ||||
| import getNoteSummary from '../../../../misc/get-note-summary'; | ||||
| import getReactionEmoji from '../../../../misc/get-reaction-emoji'; | ||||
| import getUserName from '../../../../misc/get-user-name'; | ||||
|  | ||||
| type Notification = { | ||||
| 	title: string; | ||||
| @@ -17,21 +17,21 @@ export default function(type, data): Notification { | ||||
| 			return { | ||||
| 				title: 'ファイルがアップロードされました', | ||||
| 				body: data.name, | ||||
| 				icon: data.url + '?thumbnail&size=64' | ||||
| 				icon: data.url | ||||
| 			}; | ||||
|  | ||||
| 		case 'unread_messaging_message': | ||||
| 			return { | ||||
| 				title: `${getUserName(data.user)}さんからメッセージ:`, | ||||
| 				body: data.text, // TODO: getMessagingMessageSummary(data), | ||||
| 				icon: data.user.avatarUrl + '?thumbnail&size=64' | ||||
| 				icon: data.user.avatarUrl | ||||
| 			}; | ||||
|  | ||||
| 		case 'reversi_invited': | ||||
| 			return { | ||||
| 				title: '対局への招待があります', | ||||
| 				body: `${getUserName(data.parent)}さんから`, | ||||
| 				icon: data.parent.avatarUrl + '?thumbnail&size=64' | ||||
| 				icon: data.parent.avatarUrl | ||||
| 			}; | ||||
|  | ||||
| 		case 'notification': | ||||
| @@ -40,28 +40,28 @@ export default function(type, data): Notification { | ||||
| 					return { | ||||
| 						title: `${getUserName(data.user)}さんから:`, | ||||
| 						body: getNoteSummary(data), | ||||
| 						icon: data.user.avatarUrl + '?thumbnail&size=64' | ||||
| 						icon: data.user.avatarUrl | ||||
| 					}; | ||||
|  | ||||
| 				case 'reply': | ||||
| 					return { | ||||
| 						title: `${getUserName(data.user)}さんから返信:`, | ||||
| 						body: getNoteSummary(data), | ||||
| 						icon: data.user.avatarUrl + '?thumbnail&size=64' | ||||
| 						icon: data.user.avatarUrl | ||||
| 					}; | ||||
|  | ||||
| 				case 'quote': | ||||
| 					return { | ||||
| 						title: `${getUserName(data.user)}さんが引用:`, | ||||
| 						body: getNoteSummary(data), | ||||
| 						icon: data.user.avatarUrl + '?thumbnail&size=64' | ||||
| 						icon: data.user.avatarUrl | ||||
| 					}; | ||||
|  | ||||
| 				case 'reaction': | ||||
| 					return { | ||||
| 						title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`, | ||||
| 						body: getNoteSummary(data.note), | ||||
| 						icon: data.user.avatarUrl + '?thumbnail&size=64' | ||||
| 						icon: data.user.avatarUrl | ||||
| 					}; | ||||
|  | ||||
| 				default: | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import Stream from './stream'; | ||||
| import MiOS from '../../../mios'; | ||||
| import Stream from '../../stream'; | ||||
| import MiOS from '../../../../../mios'; | ||||
| 
 | ||||
| export class ReversiGameStream extends Stream { | ||||
| 	constructor(os: MiOS, me, game) { | ||||
| 		super(os, 'reversi-game', { | ||||
| 		super(os, 'games/reversi-game', { | ||||
| 			i: me ? me.token : null, | ||||
| 			game: game.id | ||||
| 		}); | ||||
| @@ -1,10 +1,10 @@ | ||||
| import StreamManager from './stream-manager'; | ||||
| import Stream from './stream'; | ||||
| import MiOS from '../../../mios'; | ||||
| import StreamManager from '../../stream-manager'; | ||||
| import Stream from '../../stream'; | ||||
| import MiOS from '../../../../../mios'; | ||||
| 
 | ||||
| export class ReversiStream extends Stream { | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(os, 'reversi', { | ||||
| 		super(os, 'games/reversi', { | ||||
| 			i: me.token | ||||
| 		}); | ||||
| 	} | ||||
							
								
								
									
										34
									
								
								src/client/app/common/scripts/streaming/hybrid-timeline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | ||||
| import Stream from './stream'; | ||||
| import StreamManager from './stream-manager'; | ||||
| import MiOS from '../../../mios'; | ||||
|  | ||||
| /** | ||||
|  * Hybrid timeline stream connection | ||||
|  */ | ||||
| export class HybridTimelineStream extends Stream { | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(os, 'hybrid-timeline', { | ||||
| 			i: me.token | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| export class HybridTimelineStreamManager extends StreamManager<HybridTimelineStream> { | ||||
| 	private me; | ||||
| 	private os: MiOS; | ||||
|  | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(); | ||||
|  | ||||
| 		this.me = me; | ||||
| 		this.os = os; | ||||
| 	} | ||||
|  | ||||
| 	public getConnection() { | ||||
| 		if (this.connection == null) { | ||||
| 			this.connection = new HybridTimelineStream(this.os, this.me); | ||||
| 		} | ||||
|  | ||||
| 		return this.connection; | ||||
| 	} | ||||
| } | ||||
| @@ -39,13 +39,17 @@ export default Vue.extend({ | ||||
| 		dark: { | ||||
| 			type: Boolean, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		smooth: { | ||||
| 			type: Boolean, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			now: new Date(), | ||||
| 			clock: null, | ||||
| 			enabled: true, | ||||
|  | ||||
| 			graduationsPadding: 0.5, | ||||
| 			handsPadding: 1, | ||||
| @@ -74,6 +78,9 @@ export default Vue.extend({ | ||||
| 			return themeColor; | ||||
| 		}, | ||||
|  | ||||
| 		ms(): number { | ||||
| 			return this.now.getMilliseconds() * this.smooth; | ||||
| 		} | ||||
| 		s(): number { | ||||
| 			return this.now.getSeconds(); | ||||
| 		}, | ||||
| @@ -85,13 +92,13 @@ export default Vue.extend({ | ||||
| 		}, | ||||
|  | ||||
| 		hAngle(): number { | ||||
| 			return Math.PI * (this.h % 12 + this.m / 60) / 6; | ||||
| 			return Math.PI * (this.h % 12 + (this.m + (this.s + this.ms / 1000) / 60) / 60) / 6; | ||||
| 		}, | ||||
| 		mAngle(): number { | ||||
| 			return Math.PI * (this.m + this.s / 60) / 30; | ||||
| 			return Math.PI * (this.m + (this.s + this.ms / 1000) / 60) / 30; | ||||
| 		}, | ||||
| 		sAngle(): number { | ||||
| 			return Math.PI * this.s / 30; | ||||
| 			return Math.PI * (this.s + this.ms / 1000) / 30; | ||||
| 		}, | ||||
|  | ||||
| 		graduations(): any { | ||||
| @@ -106,11 +113,17 @@ export default Vue.extend({ | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.clock = setInterval(this.tick, 1000); | ||||
| 		const update = () => { | ||||
| 			if (this.enabled) { | ||||
| 				this.tick(); | ||||
| 				requestAnimationFrame(update); | ||||
| 			} | ||||
| 		}; | ||||
| 		update(); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		clearInterval(this.clock); | ||||
| 		this.enabled = false; | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
|   | ||||
| @@ -2,11 +2,16 @@ | ||||
| <div class="mk-autocomplete" @contextmenu.prevent="() => {}"> | ||||
| 	<ol class="users" ref="suggests" v-if="users.length > 0"> | ||||
| 		<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1"> | ||||
| 			<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/> | ||||
| 			<img class="avatar" :src="user.avatarUrl" alt=""/> | ||||
| 			<span class="name">{{ user | userName }}</span> | ||||
| 			<span class="username">@{{ user | acct }}</span> | ||||
| 		</li> | ||||
| 	</ol> | ||||
| 	<ol class="hashtags" ref="suggests" v-if="hashtags.length > 0"> | ||||
| 		<li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1"> | ||||
| 			<span class="name">{{ hashtag }}</span> | ||||
| 		</li> | ||||
| 	</ol> | ||||
| 	<ol class="emojis" ref="suggests" v-if="emojis.length > 0"> | ||||
| 		<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1"> | ||||
| 			<span class="emoji">{{ emoji.emoji }}</span> | ||||
| @@ -48,33 +53,33 @@ emjdb.sort((a, b) => a.name.length - b.name.length); | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: ['type', 'q', 'textarea', 'complete', 'close', 'x', 'y'], | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			users: [], | ||||
| 			hashtags: [], | ||||
| 			emojis: [], | ||||
| 			select: -1, | ||||
| 			emojilib | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		items(): HTMLCollection { | ||||
| 			return (this.$refs.suggests as Element).children; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	updated() { | ||||
| 		//#region 位置調整 | ||||
| 		const margin = 32; | ||||
|  | ||||
| 		if (this.x + this.$el.offsetWidth > window.innerWidth - margin) { | ||||
| 			this.$el.style.left = (this.x - this.$el.offsetWidth) + 'px'; | ||||
| 			this.$el.style.marginLeft = '-16px'; | ||||
| 		if (this.x + this.$el.offsetWidth > window.innerWidth) { | ||||
| 			this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px'; | ||||
| 		} else { | ||||
| 			this.$el.style.left = this.x + 'px'; | ||||
| 			this.$el.style.marginLeft = '0'; | ||||
| 		} | ||||
|  | ||||
| 		if (this.y + this.$el.offsetHeight > window.innerHeight - margin) { | ||||
| 		if (this.y + this.$el.offsetHeight > window.innerHeight) { | ||||
| 			this.$el.style.top = (this.y - this.$el.offsetHeight) + 'px'; | ||||
| 			this.$el.style.marginTop = '0'; | ||||
| 		} else { | ||||
| @@ -83,6 +88,7 @@ export default Vue.extend({ | ||||
| 		} | ||||
| 		//#endregion | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.textarea.addEventListener('keydown', this.onKeydown); | ||||
|  | ||||
| @@ -100,6 +106,7 @@ export default Vue.extend({ | ||||
| 			}); | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		this.textarea.removeEventListener('keydown', this.onKeydown); | ||||
|  | ||||
| @@ -107,6 +114,7 @@ export default Vue.extend({ | ||||
| 			el.removeEventListener('mousedown', this.onMousedown); | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		exec() { | ||||
| 			this.select = -1; | ||||
| @@ -117,7 +125,8 @@ export default Vue.extend({ | ||||
| 			} | ||||
|  | ||||
| 			if (this.type == 'user') { | ||||
| 				const cache = sessionStorage.getItem(this.q); | ||||
| 				const cacheKey = 'autocomplete:user:' + this.q; | ||||
| 				const cache = sessionStorage.getItem(cacheKey); | ||||
| 				if (cache) { | ||||
| 					const users = JSON.parse(cache); | ||||
| 					this.users = users; | ||||
| @@ -131,9 +140,33 @@ export default Vue.extend({ | ||||
| 						this.fetching = false; | ||||
|  | ||||
| 						// キャッシュ | ||||
| 						sessionStorage.setItem(this.q, JSON.stringify(users)); | ||||
| 						sessionStorage.setItem(cacheKey, JSON.stringify(users)); | ||||
| 					}); | ||||
| 				} | ||||
| 			} else if (this.type == 'hashtag') { | ||||
| 				if (this.q == null || this.q == '') { | ||||
| 					this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]'); | ||||
| 					this.fetching = false; | ||||
| 				} else { | ||||
| 					const cacheKey = 'autocomplete:hashtag:' + this.q; | ||||
| 					const cache = sessionStorage.getItem(cacheKey); | ||||
| 					if (cache) { | ||||
| 						const hashtags = JSON.parse(cache); | ||||
| 						this.hashtags = hashtags; | ||||
| 						this.fetching = false; | ||||
| 					} else { | ||||
| 						(this as any).api('hashtags/search', { | ||||
| 							query: this.q, | ||||
| 							limit: 30 | ||||
| 						}).then(hashtags => { | ||||
| 							this.hashtags = hashtags; | ||||
| 							this.fetching = false; | ||||
|  | ||||
| 							// キャッシュ | ||||
| 							sessionStorage.setItem(cacheKey, JSON.stringify(hashtags)); | ||||
| 						}); | ||||
| 					} | ||||
| 				} | ||||
| 			} else if (this.type == 'emoji') { | ||||
| 				const matched = []; | ||||
| 				emjdb.some(x => { | ||||
| @@ -228,12 +261,13 @@ export default Vue.extend({ | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| .mk-autocomplete | ||||
| root(isDark) | ||||
| 	position fixed | ||||
| 	z-index 65535 | ||||
| 	max-width 100% | ||||
| 	margin-top calc(1em + 8px) | ||||
| 	overflow hidden | ||||
| 	background #fff | ||||
| 	background isDark ? #313543 : #fff | ||||
| 	border solid 1px rgba(#000, 0.1) | ||||
| 	border-radius 4px | ||||
| 	transition top 0.1s ease, left 0.1s ease | ||||
| @@ -248,7 +282,8 @@ export default Vue.extend({ | ||||
| 		list-style none | ||||
|  | ||||
| 		> li | ||||
| 			display block | ||||
| 			display flex | ||||
| 			align-items center | ||||
| 			padding 4px 12px | ||||
| 			white-space nowrap | ||||
| 			overflow hidden | ||||
| @@ -259,7 +294,13 @@ export default Vue.extend({ | ||||
| 			&, * | ||||
| 				user-select none | ||||
|  | ||||
| 			* | ||||
| 				overflow hidden | ||||
| 				text-overflow ellipsis | ||||
|  | ||||
| 			&:hover | ||||
| 				background isDark ? rgba(#fff, 0.1) : rgba(#000, 0.1) | ||||
|  | ||||
| 			&[data-selected='true'] | ||||
| 				background $theme-color | ||||
|  | ||||
| @@ -275,7 +316,6 @@ export default Vue.extend({ | ||||
| 	> .users > li | ||||
|  | ||||
| 		.avatar | ||||
| 			vertical-align middle | ||||
| 			min-width 28px | ||||
| 			min-height 28px | ||||
| 			max-width 28px | ||||
| @@ -285,10 +325,15 @@ export default Vue.extend({ | ||||
|  | ||||
| 		.name | ||||
| 			margin 0 8px 0 0 | ||||
| 			color rgba(#000, 0.8) | ||||
| 			color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8) | ||||
|  | ||||
| 		.username | ||||
| 			color rgba(#000, 0.3) | ||||
| 			color isDark ? rgba(#fff, 0.3) : rgba(#000, 0.3) | ||||
|  | ||||
| 	> .hashtags > li | ||||
|  | ||||
| 		.name | ||||
| 			color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8) | ||||
|  | ||||
| 	> .emojis > li | ||||
|  | ||||
| @@ -298,10 +343,15 @@ export default Vue.extend({ | ||||
| 			width 24px | ||||
|  | ||||
| 		.name | ||||
| 			color rgba(#000, 0.8) | ||||
| 			color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8) | ||||
|  | ||||
| 		.alias | ||||
| 			margin 0 0 0 8px | ||||
| 			color rgba(#000, 0.3) | ||||
| 			color isDark ? rgba(#fff, 0.3) : rgba(#000, 0.3) | ||||
|  | ||||
| .mk-autocomplete[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .mk-autocomplete:not([data-darkmode]) | ||||
| 	root(false) | ||||
| </style> | ||||
|   | ||||
| @@ -31,7 +31,7 @@ export default Vue.extend({ | ||||
| 					: this.user.avatarColor && this.user.avatarColor.length == 3 | ||||
| 						? `rgb(${ this.user.avatarColor.join(',') })` | ||||
| 						: null, | ||||
| 				backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl }?thumbnail)`, | ||||
| 				backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl })`, | ||||
| 				borderRadius: this.$store.state.settings.circleIcons ? '100%' : null | ||||
| 			}; | ||||
| 		} | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
| 		<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">%i18n:common.reversi.opponent-turn%<mk-ellipsis/></p> | ||||
| 		<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">%i18n:common.reversi.my-turn%</p> | ||||
| 		<p class="result" v-if="game.isEnded && logPos == logs.length"> | ||||
| 			<template v-if="game.winner"><b>{{ game.winner.name }}</b>の勝ち{{ game.settings.isLlotheo ? ' (ロセオ)' : '' }}</template> | ||||
| 			<template v-if="game.winner">{{ '%i18n:common.reversi.won%'.replace('{}', game.winner.name) }}{{ game.settings.isLlotheo ? ' (ロセオ)' : '' }}</template> | ||||
| 			<template v-else>%i18n:common.reversi.drawn%</template> | ||||
| 		</p> | ||||
| 	</div> | ||||
| @@ -26,8 +26,8 @@ | ||||
| 						:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }" | ||||
| 						@click="set(i)" | ||||
| 						:title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"> | ||||
| 					<img v-if="stone === true" :src="`${blackUser.avatarUrl}?thumbnail&size=128`" alt=""> | ||||
| 					<img v-if="stone === false" :src="`${whiteUser.avatarUrl}?thumbnail&size=128`" alt=""> | ||||
| 					<img v-if="stone === true" :src="blackUser.avatarUrl" alt=""> | ||||
| 					<img v-if="stone === false" :src="whiteUser.avatarUrl" alt=""> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="labels-y" v-if="this.$store.state.settings.reversiBoardLabels"> | ||||
| @@ -58,8 +58,8 @@ | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import * as CRC32 from 'crc-32'; | ||||
| import Reversi, { Color } from '../../../../../reversi/core'; | ||||
| import { url } from '../../../config'; | ||||
| import Reversi, { Color } from '../../../../../../../games/reversi/core'; | ||||
| import { url } from '../../../../../config'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	props: ['initGame', 'connection'], | ||||
| @@ -105,13 +105,14 @@ export default Vue.extend({ | ||||
| 			} | ||||
| 		}, | ||||
| 		isMyTurn(): boolean { | ||||
| 			if (this.turnUser == null) return null; | ||||
| 			if (!this.iAmPlayer) return false; | ||||
| 			if (this.turnUser == null) return false; | ||||
| 			return this.turnUser.id == this.$store.state.i.id; | ||||
| 		}, | ||||
| 		cellsStyle(): any { | ||||
| 			return { | ||||
| 				'grid-template-rows': `repeat(${ this.game.settings.map.length }, 1fr)`, | ||||
| 				'grid-template-columns': `repeat(${ this.game.settings.map[0].length }, 1fr)` | ||||
| 				'grid-template-rows': `repeat(${this.game.settings.map.length}, 1fr)`, | ||||
| 				'grid-template-columns': `repeat(${this.game.settings.map[0].length}, 1fr)` | ||||
| 			}; | ||||
| 		} | ||||
| 	}, | ||||
| @@ -9,7 +9,7 @@ | ||||
| import Vue from 'vue'; | ||||
| import XGame from './reversi.game.vue'; | ||||
| import XRoom from './reversi.room.vue'; | ||||
| import { ReversiGameStream } from '../../scripts/streaming/reversi-game'; | ||||
| import { ReversiGameStream } from '../../../../scripts/streaming/games/reversi/reversi-game'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| @@ -94,7 +94,7 @@ | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import * as maps from '../../../../../reversi/maps'; | ||||
| import * as maps from '../../../../../../../games/reversi/maps'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	props: ['game', 'connection'], | ||||
| @@ -112,7 +112,7 @@ export default Vue.extend({ | ||||
| 
 | ||||
| 	computed: { | ||||
| 		mapCategories(): string[] { | ||||
| 			const categories = Object.entries(maps).map(x => x[1].category); | ||||
| 			const categories = Object.values(maps).map(x => x.category); | ||||
| 			return categories.filter((item, pos) => categories.indexOf(item) == pos); | ||||
| 		}, | ||||
| 		isAccepted(): boolean { | ||||
| @@ -179,8 +179,8 @@ export default Vue.extend({ | ||||
| 			if (this.game.settings.map == null) { | ||||
| 				this.mapName = null; | ||||
| 			} else { | ||||
| 				const foundMap = Object.entries(maps).find(x => x[1].data.join('') == this.game.settings.map.join('')); | ||||
| 				this.mapName = foundMap ? foundMap[1].name : '-Custom-'; | ||||
| 				const found = Object.values(maps).find(x => x.data.join('') == this.game.settings.map.join('')); | ||||
| 				this.mapName = found ? found.name : '-Custom-'; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| @@ -206,7 +206,7 @@ export default Vue.extend({ | ||||
| 			if (v == null) { | ||||
| 				this.game.settings.map = null; | ||||
| 			} else { | ||||
| 				this.game.settings.map = Object.entries(maps).find(x => x[1].name == v)[1].data; | ||||
| 				this.game.settings.map = Object.values(maps).find(x => x.name == v).data; | ||||
| 			} | ||||
| 			this.$forceUpdate(); | ||||
| 			this.updateSettings(); | ||||
| @@ -217,9 +217,9 @@ export default Vue.extend({ | ||||
| 			const y = Math.floor(pos / this.game.settings.map[0].length); | ||||
| 			const newPixel = | ||||
| 				pixel == ' ' ? '-' : | ||||
| 				pixel == '-' ? 'b' : | ||||
| 				pixel == 'b' ? 'w' : | ||||
| 				' '; | ||||
| 					pixel == '-' ? 'b' : | ||||
| 						pixel == 'b' ? 'w' : | ||||
| 							' '; | ||||
| 			const line = this.game.settings.map[y].split(''); | ||||
| 			line[x] = newPixel; | ||||
| 			this.$set(this.game.settings.map, y, line.join('')); | ||||
| @@ -67,7 +67,9 @@ export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XGameroom | ||||
| 	}, | ||||
| 
 | ||||
| 	props: ['initGame'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			game: null, | ||||
| @@ -82,63 +84,73 @@ export default Vue.extend({ | ||||
| 			pingClock: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		game(g) { | ||||
| 			this.$emit('gamed', g); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		if (this.initGame) { | ||||
| 			this.game = this.initGame; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.streams.reversiStream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.streams.reversiStream.use(); | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.streams.reversiStream.getConnection(); | ||||
| 			this.connectionId = (this as any).os.streams.reversiStream.use(); | ||||
| 
 | ||||
| 		this.connection.on('matched', this.onMatched); | ||||
| 		this.connection.on('invited', this.onInvited); | ||||
| 			this.connection.on('matched', this.onMatched); | ||||
| 			this.connection.on('invited', this.onInvited); | ||||
| 
 | ||||
| 		(this as any).api('reversi/games', { | ||||
| 			my: true | ||||
| 		}).then(games => { | ||||
| 			this.myGames = games; | ||||
| 		}); | ||||
| 			(this as any).api('games/reversi/games', { | ||||
| 				my: true | ||||
| 			}).then(games => { | ||||
| 				this.myGames = games; | ||||
| 			}); | ||||
| 
 | ||||
| 		(this as any).api('reversi/games').then(games => { | ||||
| 			(this as any).api('games/reversi/invitations').then(invitations => { | ||||
| 				this.invitations = this.invitations.concat(invitations); | ||||
| 			}); | ||||
| 
 | ||||
| 			this.pingClock = setInterval(() => { | ||||
| 				if (this.matching) { | ||||
| 					this.connection.send({ | ||||
| 						type: 'ping', | ||||
| 						id: this.matching.id | ||||
| 					}); | ||||
| 				} | ||||
| 			}, 3000); | ||||
| 		} | ||||
| 
 | ||||
| 		(this as any).api('games/reversi/games').then(games => { | ||||
| 			this.games = games; | ||||
| 			this.gamesFetching = false; | ||||
| 		}); | ||||
| 
 | ||||
| 		(this as any).api('reversi/invitations').then(invitations => { | ||||
| 			this.invitations = this.invitations.concat(invitations); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.pingClock = setInterval(() => { | ||||
| 			if (this.matching) { | ||||
| 				this.connection.send({ | ||||
| 					type: 'ping', | ||||
| 					id: this.matching.id | ||||
| 				}); | ||||
| 			} | ||||
| 		}, 3000); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('matched', this.onMatched); | ||||
| 		this.connection.off('invited', this.onInvited); | ||||
| 		(this as any).os.streams.reversiStream.dispose(this.connectionId); | ||||
| 		if (this.connection) { | ||||
| 			this.connection.off('matched', this.onMatched); | ||||
| 			this.connection.off('invited', this.onInvited); | ||||
| 			(this as any).os.streams.reversiStream.dispose(this.connectionId); | ||||
| 
 | ||||
| 		clearInterval(this.pingClock); | ||||
| 			clearInterval(this.pingClock); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		go(game) { | ||||
| 			(this as any).api('reversi/games/show', { | ||||
| 			(this as any).api('games/reversi/games/show', { | ||||
| 				gameId: game.id | ||||
| 			}).then(game => { | ||||
| 				this.matching = null; | ||||
| 				this.game = game; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		match() { | ||||
| 			(this as any).apis.input({ | ||||
| 				title: 'ユーザー名を入力してください' | ||||
| @@ -146,7 +158,7 @@ export default Vue.extend({ | ||||
| 				(this as any).api('users/show', { | ||||
| 					username | ||||
| 				}).then(user => { | ||||
| 					(this as any).api('reversi/match', { | ||||
| 					(this as any).api('games/reversi/match', { | ||||
| 						userId: user.id | ||||
| 					}).then(res => { | ||||
| 						if (res == null) { | ||||
| @@ -158,12 +170,14 @@ export default Vue.extend({ | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		cancel() { | ||||
| 			this.matching = null; | ||||
| 			(this as any).api('reversi/match/cancel'); | ||||
| 			(this as any).api('games/reversi/match/cancel'); | ||||
| 		}, | ||||
| 
 | ||||
| 		accept(invitation) { | ||||
| 			(this as any).api('reversi/match', { | ||||
| 			(this as any).api('games/reversi/match', { | ||||
| 				userId: invitation.parent.id | ||||
| 			}).then(game => { | ||||
| 				if (game) { | ||||
| @@ -172,10 +186,12 @@ export default Vue.extend({ | ||||
| 				} | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		onMatched(game) { | ||||
| 			this.matching = null; | ||||
| 			this.game = game; | ||||
| 		}, | ||||
| 
 | ||||
| 		onInvited(invite) { | ||||
| 			this.invitations.unshift(invite); | ||||
| 		} | ||||
| @@ -27,7 +27,7 @@ import urlPreview from './url-preview.vue'; | ||||
| import twitterSetting from './twitter-setting.vue'; | ||||
| import fileTypeIcon from './file-type-icon.vue'; | ||||
| import Switch from './switch.vue'; | ||||
| import Reversi from './reversi.vue'; | ||||
| import Reversi from './games/reversi/reversi.vue'; | ||||
| import welcomeTimeline from './welcome-timeline.vue'; | ||||
| import uiInput from './ui/input.vue'; | ||||
| import uiButton from './ui/button.vue'; | ||||
|   | ||||
| @@ -46,33 +46,45 @@ export default Vue.extend({ | ||||
| 		display grid | ||||
| 		grid-gap 4px | ||||
|  | ||||
| 		> * | ||||
| 			overflow hidden | ||||
| 			border-radius 4px | ||||
|  | ||||
| 		&[data-count="1"] | ||||
| 			grid-template-rows 1fr | ||||
|  | ||||
| 		&[data-count="2"] | ||||
| 			grid-template-columns 1fr 1fr | ||||
| 			grid-template-rows 1fr | ||||
|  | ||||
| 		&[data-count="3"] | ||||
| 			grid-template-columns 1fr 0.5fr | ||||
| 			grid-template-rows 1fr 1fr | ||||
| 			:nth-child(1) | ||||
|  | ||||
| 			> *:nth-child(1) | ||||
| 				grid-row 1 / 3 | ||||
| 			:nth-child(3) | ||||
|  | ||||
| 			> *:nth-child(3) | ||||
| 				grid-column 2 / 3 | ||||
| 				grid-row 2 / 3 | ||||
|  | ||||
| 		&[data-count="4"] | ||||
| 			grid-template-columns 1fr 1fr | ||||
| 			grid-template-rows 1fr 1fr | ||||
|  | ||||
| 		:nth-child(1) | ||||
| 		> *:nth-child(1) | ||||
| 			grid-column 1 / 2 | ||||
| 			grid-row 1 / 2 | ||||
| 		:nth-child(2) | ||||
|  | ||||
| 		> *:nth-child(2) | ||||
| 			grid-column 2 / 3 | ||||
| 			grid-row 1 / 2 | ||||
| 		:nth-child(3) | ||||
|  | ||||
| 		> *:nth-child(3) | ||||
| 			grid-column 1 / 2 | ||||
| 			grid-row 2 / 3 | ||||
| 		:nth-child(4) | ||||
|  | ||||
| 		> *:nth-child(4) | ||||
| 			grid-column 2 / 3 | ||||
| 			grid-row 2 / 3 | ||||
|  | ||||
|   | ||||
| @@ -119,7 +119,7 @@ export default Vue.extend({ | ||||
| 		}, | ||||
|  | ||||
| 		onKeypress(e) { | ||||
| 			if ((e.which == 10 || e.which == 13) && e.ctrlKey) { | ||||
| 			if ((e.which == 10 || e.which == 13) && e.ctrlKey && this.canSend) { | ||||
| 				this.send(); | ||||
| 			} | ||||
| 		}, | ||||
|   | ||||
| @@ -3,10 +3,9 @@ | ||||
| 	<mk-avatar class="avatar" :user="message.user" target="_blank"/> | ||||
| 	<div class="content"> | ||||
| 		<div class="balloon" :data-no-text="message.text == null"> | ||||
| 			<p class="read" v-if="isMe && message.isRead">%i18n:@is-read%</p> | ||||
| 			<button class="delete-button" v-if="isMe" title="%i18n:common.delete%"> | ||||
| 			<!-- <button class="delete-button" v-if="isMe" title="%i18n:common.delete%"> | ||||
| 				<img src="/assets/desktop/messaging/delete.png" alt="Delete"/> | ||||
| 			</button> | ||||
| 			</button> --> | ||||
| 			<div class="content" v-if="!message.isDeleted"> | ||||
| 				<misskey-flavored-markdown class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/> | ||||
| 				<div class="file" v-if="message.file"> | ||||
| @@ -23,6 +22,7 @@ | ||||
| 		<div></div> | ||||
| 		<mk-url-preview v-for="url in urls" :url="url" :key="url"/> | ||||
| 		<footer> | ||||
| 			<span class="read" v-if="isMe && message.isRead">%i18n:@is-read%</span> | ||||
| 			<mk-time :time="message.createdAt"/> | ||||
| 			<template v-if="message.is_edited">%fa:pencil-alt%</template> | ||||
| 		</footer> | ||||
| @@ -120,17 +120,6 @@ root(isDark) | ||||
| 					height 16px | ||||
| 					cursor pointer | ||||
|  | ||||
| 			> .read | ||||
| 				user-select none | ||||
| 				display block | ||||
| 				position absolute | ||||
| 				z-index 1 | ||||
| 				bottom -4px | ||||
| 				left -12px | ||||
| 				margin 0 | ||||
| 				color isDark ? rgba(#fff, 0.5) : rgba(#000, 0.5) | ||||
| 				font-size 11px | ||||
|  | ||||
| 			> .content | ||||
|  | ||||
| 				> .is-deleted | ||||
| @@ -258,6 +247,12 @@ root(isDark) | ||||
| 			> footer | ||||
| 				text-align right | ||||
|  | ||||
| 				> .read | ||||
| 					user-select none | ||||
| 					margin 0 4px 0 0 | ||||
| 					color isDark ? rgba(#fff, 0.5) : rgba(#000, 0.5) | ||||
| 					font-size 11px | ||||
|  | ||||
| 	&[data-is-deleted] | ||||
| 		> .baloon | ||||
| 			opacity 0.5 | ||||
|   | ||||
| @@ -51,7 +51,7 @@ | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import getAcct from '../../../../../acct/render'; | ||||
| import getAcct from '../../../../../misc/acct/render'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import Vue from 'vue'; | ||||
| import * as emojilib from 'emojilib'; | ||||
| import parse from '../../../../../mfm/parse'; | ||||
| import getAcct from '../../../../../acct/render'; | ||||
| import getAcct from '../../../../../misc/acct/render'; | ||||
| import { url } from '../../../config'; | ||||
| import MkUrl from './url.vue'; | ||||
| import MkGoogle from './google.vue'; | ||||
| @@ -92,7 +92,7 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 				case 'hashtag': | ||||
| 					return createElement('a', { | ||||
| 						attrs: { | ||||
| 							href: `${url}/tags/${token.hashtag}`, | ||||
| 							href: `${url}/tags/${encodeURIComponent(token.hashtag)}`, | ||||
| 							target: '_blank' | ||||
| 						} | ||||
| 					}, token.content); | ||||
|   | ||||
| @@ -2,9 +2,9 @@ | ||||
| <span class="mk-nav"> | ||||
| 	<a :href="aboutUrl">%i18n:@about%</a> | ||||
| 	<i>・</i> | ||||
| 	<a href="https://github.com/syuilo/misskey">%i18n:@repository%</a> | ||||
| 	<a :href="repositoryUrl">%i18n:@repository%</a> | ||||
| 	<i>・</i> | ||||
| 	<a href="https://github.com/syuilo/misskey/issues/new" target="_blank">%i18n:@feedback%</a> | ||||
| 	<a :href="feedbackUrl" target="_blank">%i18n:@feedback%</a> | ||||
| 	<i>・</i> | ||||
| 	<a :href="devUrl">%i18n:@develop%</a> | ||||
| 	<i>・</i> | ||||
| @@ -14,7 +14,7 @@ | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { docsUrl, statsUrl, statusUrl, devUrl, lang } from '../../../config'; | ||||
| import { docsUrl, statsUrl, statusUrl, devUrl, repositoryUrl, feedbackUrl, lang } from '../../../config'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| @@ -22,7 +22,9 @@ export default Vue.extend({ | ||||
| 			aboutUrl: `${docsUrl}/${lang}/about`, | ||||
| 			statsUrl, | ||||
| 			statusUrl, | ||||
| 			devUrl | ||||
| 			devUrl, | ||||
| 			repositoryUrl: repositoryUrl || `https://github.com/syuilo/misskey`, | ||||
| 			feedbackUrl: feedbackUrl || `https://github.com/syuilo/misskey/issues/new` | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| <header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu"> | ||||
| 	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/> | ||||
| 	<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link> | ||||
| 	<span class="is-verified" v-if="note.user.isVerified" title="%i18n:common.verified-user%">%fa:bookmark%</span> | ||||
| 	<span class="is-admin" v-if="note.user.isAdmin">admin</span> | ||||
| 	<span class="is-bot" v-if="note.user.isBot">bot</span> | ||||
| 	<span class="is-cat" v-if="note.user.isCat">cat</span> | ||||
| @@ -69,6 +70,10 @@ root(isDark) | ||||
| 		&:hover | ||||
| 			text-decoration underline | ||||
|  | ||||
| 	> .is-verified | ||||
| 		margin-right 8px | ||||
| 		color #4dabf7 | ||||
|  | ||||
| 	> .is-admin | ||||
| 	> .is-bot | ||||
| 	> .is-cat | ||||
|   | ||||
| @@ -183,7 +183,7 @@ root(isDark) | ||||
| 				border-right solid $balloon-size transparent | ||||
| 				border-bottom solid $balloon-size $bgcolor | ||||
|  | ||||
| 		&.compact | ||||
| 		&.big | ||||
| 			> div | ||||
| 				width 280px | ||||
|  | ||||
|   | ||||
| @@ -29,11 +29,7 @@ | ||||
| 			<p slot="text" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@password-not-matched%</p> | ||||
| 		</div> | ||||
| 	</ui-input> | ||||
| 	<div class="g-recaptcha" :data-sitekey="recaptchaSitekey" style="margin: 16px 0;"></div> | ||||
| 	<label class="agree-tou" style="display: block; margin: 16px 0;"> | ||||
| 		<input name="agree-tou" type="checkbox" required/> | ||||
| 		<p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p> | ||||
| 	</label> | ||||
| 	<div v-if="recaptchaSitekey != null" class="g-recaptcha" :data-sitekey="recaptchaSitekey" style="margin: 16px 0;"></div> | ||||
| 	<ui-button type="submit">%i18n:@create%</ui-button> | ||||
| </form> | ||||
| </template> | ||||
| @@ -41,7 +37,7 @@ | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| const getPasswordStrength = require('syuilo-password-strength'); | ||||
| import { host, url, docsUrl, lang, recaptchaSitekey } from '../../../config'; | ||||
| import { host, url, recaptchaSitekey } from '../../../config'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| @@ -51,7 +47,6 @@ export default Vue.extend({ | ||||
| 			password: '', | ||||
| 			retypedPassword: '', | ||||
| 			url, | ||||
| 			touUrl: `${docsUrl}/${lang}/tou`, | ||||
| 			recaptchaSitekey, | ||||
| 			usernameState: null, | ||||
| 			passwordStrength: '', | ||||
| @@ -115,7 +110,7 @@ export default Vue.extend({ | ||||
| 			(this as any).api('signup', { | ||||
| 				username: this.username, | ||||
| 				password: this.password, | ||||
| 				'g-recaptcha-response': (window as any).grecaptcha.getResponse() | ||||
| 				'g-recaptcha-response': recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null | ||||
| 			}).then(() => { | ||||
| 				(this as any).api('signin', { | ||||
| 					username: this.username, | ||||
| @@ -126,15 +121,19 @@ export default Vue.extend({ | ||||
| 			}).catch(() => { | ||||
| 				alert('%i18n:@some-error%'); | ||||
|  | ||||
| 				(window as any).grecaptcha.reset(); | ||||
| 				if (recaptchaSitekey != null) { | ||||
| 					(window as any).grecaptcha.reset(); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 	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); | ||||
| 		if (recaptchaSitekey != null) { | ||||
| 			const head = document.getElementsByTagName('head')[0]; | ||||
| 			const script = document.createElement('script'); | ||||
| 			script.setAttribute('src', 'https://www.google.com/recaptcha/api.js'); | ||||
| 			head.appendChild(script); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| @@ -144,22 +143,4 @@ export default Vue.extend({ | ||||
|  | ||||
| .mk-signup | ||||
| 	min-width 302px | ||||
|  | ||||
| 	.agree-tou | ||||
| 		padding 4px | ||||
| 		border-radius 4px | ||||
|  | ||||
| 		&:hover | ||||
| 			background #f4f4f4 | ||||
|  | ||||
| 		&:active | ||||
| 			background #eee | ||||
|  | ||||
| 		&, * | ||||
| 			cursor pointer | ||||
|  | ||||
| 		p | ||||
| 			display inline | ||||
| 			color #555 | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -2,6 +2,11 @@ | ||||
| <iframe v-if="youtubeId" type="text/html" height="250" | ||||
| 	:src="`https://www.youtube.com/embed/${youtubeId}?origin=${misskeyUrl}`" | ||||
| 	frameborder="0"/> | ||||
| <div v-else-if="tweetUrl && detail" class="twitter"> | ||||
| 	<blockquote ref="tweet" class="twitter-tweet" :data-theme="$store.state.device.darkmode ? 'dark' : null"> | ||||
| 		<a :href="url"></a> | ||||
| 	</blockquote> | ||||
| </div> | ||||
| <div v-else class="mk-url-preview"> | ||||
| 	<a :href="url" target="_blank" :title="url" v-if="!fetching"> | ||||
| 		<div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div> | ||||
| @@ -24,7 +29,17 @@ import Vue from 'vue'; | ||||
| import { url as misskeyUrl } from '../../../config'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: ['url'], | ||||
| 	props: { | ||||
| 		url: { | ||||
| 			type: String, | ||||
| 			require: true | ||||
| 		}, | ||||
| 		detail: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| @@ -34,6 +49,7 @@ export default Vue.extend({ | ||||
| 			icon: null, | ||||
| 			sitename: null, | ||||
| 			youtubeId: null, | ||||
| 			tweetUrl: null, | ||||
| 			misskeyUrl | ||||
| 		}; | ||||
| 	}, | ||||
| @@ -44,6 +60,25 @@ export default Vue.extend({ | ||||
| 			this.youtubeId = url.searchParams.get('v'); | ||||
| 		} else if (url.hostname == 'youtu.be') { | ||||
| 			this.youtubeId = url.pathname; | ||||
| 		} else if (this.detail && url.hostname == 'twitter.com' && /^\/.+\/status(es)?\/\d+/.test(url.pathname)) { | ||||
| 			this.tweetUrl = url; | ||||
| 			const twttr = (window as any).twttr || {}; | ||||
| 			const loadTweet = () => twttr.widgets.load(this.$refs.tweet); | ||||
|  | ||||
| 			if (twttr.widgets) { | ||||
| 				Vue.nextTick(loadTweet); | ||||
| 			} else { | ||||
| 				const wjsId = 'twitter-wjs'; | ||||
| 				if (!document.getElementById(wjsId)) { | ||||
| 					const head = document.getElementsByTagName('head')[0]; | ||||
| 					const script = document.createElement('script'); | ||||
| 					script.setAttribute('id', wjsId); | ||||
| 					script.setAttribute('src', 'https://platform.twitter.com/widgets.js'); | ||||
| 					head.appendChild(script); | ||||
| 				} | ||||
| 				twttr.ready = loadTweet; | ||||
| 				(window as any).twttr = twttr; | ||||
| 			} | ||||
| 		} else { | ||||
| 			fetch('/url?url=' + encodeURIComponent(this.url)).then(res => { | ||||
| 				res.json().then(info => { | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import * as getCaretCoordinates from 'textarea-caret'; | ||||
| import MkAutocomplete from '../components/autocomplete.vue'; | ||||
| import renderAcct from '../../../../../misc/acct/render'; | ||||
|  | ||||
| export default { | ||||
| 	bind(el, binding, vn) { | ||||
| @@ -67,15 +68,30 @@ class Autocomplete { | ||||
| 	 * テキスト入力時 | ||||
| 	 */ | ||||
| 	private onInput() { | ||||
| 		const caret = this.textarea.selectionStart; | ||||
| 		const text = this.text.substr(0, caret); | ||||
| 		const caretPos = this.textarea.selectionStart; | ||||
| 		const text = this.text.substr(0, caretPos).split('\n').pop(); | ||||
|  | ||||
| 		const mentionIndex = text.lastIndexOf('@'); | ||||
| 		const hashtagIndex = text.lastIndexOf('#'); | ||||
| 		const emojiIndex = text.lastIndexOf(':'); | ||||
|  | ||||
| 		const max = Math.max( | ||||
| 			mentionIndex, | ||||
| 			hashtagIndex, | ||||
| 			emojiIndex); | ||||
|  | ||||
| 		if (max == -1) { | ||||
| 			this.close(); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		const isMention = mentionIndex != -1; | ||||
| 		const isHashtag = hashtagIndex != -1; | ||||
| 		const isEmoji = emojiIndex != -1; | ||||
|  | ||||
| 		let opened = false; | ||||
|  | ||||
| 		if (mentionIndex != -1 && mentionIndex > emojiIndex) { | ||||
| 		if (isMention) { | ||||
| 			const username = text.substr(mentionIndex + 1); | ||||
| 			if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) { | ||||
| 				this.open('user', username); | ||||
| @@ -83,7 +99,15 @@ class Autocomplete { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (emojiIndex != -1 && emojiIndex > mentionIndex) { | ||||
| 		if (isHashtag && opened == false) { | ||||
| 			const hashtag = text.substr(hashtagIndex + 1); | ||||
| 			if (!hashtag.includes(' ')) { | ||||
| 				this.open('hashtag', hashtag); | ||||
| 				opened = true; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (isEmoji && opened == false) { | ||||
| 			const emoji = text.substr(emojiIndex + 1); | ||||
| 			if (emoji != '' && emoji.match(/^[\+\-a-z0-9_]+$/)) { | ||||
| 				this.open('emoji', emoji); | ||||
| @@ -164,13 +188,31 @@ class Autocomplete { | ||||
| 			const trimmedBefore = before.substring(0, before.lastIndexOf('@')); | ||||
| 			const after = source.substr(caret); | ||||
|  | ||||
| 			const acct = renderAcct(value); | ||||
|  | ||||
| 			// 挿入 | ||||
| 			this.text = trimmedBefore + '@' + value.username + ' ' + after; | ||||
| 			this.text = trimmedBefore + '@' + acct + ' ' + after; | ||||
|  | ||||
| 			// キャレットを戻す | ||||
| 			this.vm.$nextTick(() => { | ||||
| 				this.textarea.focus(); | ||||
| 				const pos = trimmedBefore.length + (value.username.length + 2); | ||||
| 				const pos = trimmedBefore.length + (acct.length + 2); | ||||
| 				this.textarea.setSelectionRange(pos, pos); | ||||
| 			}); | ||||
| 		} else if (type == 'hashtag') { | ||||
| 			const source = this.text; | ||||
|  | ||||
| 			const before = source.substr(0, caret); | ||||
| 			const trimmedBefore = before.substring(0, before.lastIndexOf('#')); | ||||
| 			const after = source.substr(caret); | ||||
|  | ||||
| 			// 挿入 | ||||
| 			this.text = trimmedBefore + '#' + value + ' ' + after; | ||||
|  | ||||
| 			// キャレットを戻す | ||||
| 			this.vm.$nextTick(() => { | ||||
| 				this.textarea.focus(); | ||||
| 				const pos = trimmedBefore.length + (value.length + 2); | ||||
| 				this.textarea.setSelectionRange(pos, pos); | ||||
| 			}); | ||||
| 		} else if (type == 'emoji') { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import Vue from 'vue'; | ||||
| import getAcct from '../../../../../acct/render'; | ||||
| import getUserName from '../../../../../renderers/get-user-name'; | ||||
| import getAcct from '../../../../../misc/acct/render'; | ||||
| import getUserName from '../../../../../misc/get-user-name'; | ||||
|  | ||||
| Vue.filter('acct', user => { | ||||
| 	return getAcct(user); | ||||
|   | ||||
| @@ -31,8 +31,8 @@ | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import parseAcct from '../../../../../acct/parse'; | ||||
| import getUserName from '../../../../../renderers/get-user-name'; | ||||
| import parseAcct from '../../../../../misc/acct/parse'; | ||||
| import getUserName from '../../../../../misc/get-user-name'; | ||||
| import Progress from '../../../common/scripts/loading'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| <template> | ||||
| <div class="mkw-analog-clock"> | ||||
| 	<mk-widget-container :naked="props.naked" :show-header="false"> | ||||
| 	<mk-widget-container :naked="!(props.design % 2)" :show-header="false"> | ||||
| 		<div class="mkw-analog-clock--body"> | ||||
| 			<mk-analog-clock :dark="$store.state.device.darkmode"/> | ||||
| 			<mk-analog-clock :dark="$store.state.device.darkmode" :smooth="!(props.design && ~props.design)"/> | ||||
| 		</div> | ||||
| 	</mk-widget-container> | ||||
| </div> | ||||
| @@ -13,12 +13,13 @@ import define from '../../../common/define-widget'; | ||||
| export default define({ | ||||
| 	name: 'analog-clock', | ||||
| 	props: () => ({ | ||||
| 		naked: false | ||||
| 		design: -1 | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	methods: { | ||||
| 		func() { | ||||
| 			this.props.naked = !this.props.naked; | ||||
| 			if (++this.props.design > 2) | ||||
| 				this.props.design = -1; | ||||
| 			this.save(); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -175,6 +175,7 @@ root(isDark) | ||||
| 					> .val | ||||
| 						height 4px | ||||
| 						background $theme-color | ||||
| 						transition width .3s cubic-bezier(0.23, 1, 0.32, 1) | ||||
|  | ||||
| 				&:nth-child(1) | ||||
| 					> .meter > .val | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
| 			<div> | ||||
| 				<div v-for="stat in stats" :key="stat.tag"> | ||||
| 					<div class="tag"> | ||||
| 						<router-link :to="`/tags/${ stat.tag }`" :title="stat.tag">#{{ stat.tag }}</router-link> | ||||
| 						<router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> | ||||
| 						<p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p> | ||||
| 					</div> | ||||
| 					<x-chart class="chart" :src="stat.chart"/> | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|  | ||||
| 		<p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> | ||||
| 		<div :class="$style.stream" v-if="!fetching && images.length > 0"> | ||||
| 			<div v-for="image in images" :class="$style.img" :style="`background-image: url(${image.url}?thumbnail&size=256)`"></div> | ||||
| 			<div v-for="image in images" :class="$style.img" :style="`background-image: url(${image.url})`"></div> | ||||
| 		</div> | ||||
| 		<p :class="$style.empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p> | ||||
| 	</mk-widget-container> | ||||
|   | ||||
| @@ -102,7 +102,6 @@ export default Vue.extend({ | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		onStats(stats) { | ||||
| 			stats.mem.used = stats.mem.total - stats.mem.free; | ||||
| 			this.stats.push(stats); | ||||
| 			if (this.stats.length > 50) this.stats.shift(); | ||||
|  | ||||
| @@ -111,8 +110,8 @@ export default Vue.extend({ | ||||
| 			this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); | ||||
| 			this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); | ||||
|  | ||||
| 			this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.cpuPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; | ||||
| 			this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.memPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; | ||||
| 			this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; | ||||
| 			this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; | ||||
|  | ||||
| 			this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0]; | ||||
| 			this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1]; | ||||
|   | ||||
| @@ -35,7 +35,7 @@ export default Vue.extend({ | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		onStats(stats) { | ||||
| 			stats.mem.used = stats.mem.total - stats.mem.free; | ||||
| 			stats.mem.free = stats.mem.total - stats.mem.used; | ||||
| 			this.usage = stats.mem.used / stats.mem.total; | ||||
| 			this.total = stats.mem.total; | ||||
| 			this.used = stats.mem.used; | ||||
|   | ||||
| @@ -2,10 +2,10 @@ | ||||
| <div class="mkw-slideshow" :data-mobile="platform == 'mobile'"> | ||||
| 	<div @click="choose"> | ||||
| 		<p v-if="props.folder === undefined"> | ||||
| 			<template v-if="isCustomizeMode">フォルダを指定するには、カスタマイズモードを終了してください</template> | ||||
| 			<template v-else>クリックしてフォルダを指定してください</template> | ||||
| 			<template v-if="isCustomizeMode">%i18n:@folder-customize-mode%</template> | ||||
| 			<template v-else>%i18n:@folder%</template> | ||||
| 		</p> | ||||
| 		<p v-if="props.folder !== undefined && images.length == 0 && !fetching">このフォルダには画像がありません</p> | ||||
| 		<p v-if="props.folder !== undefined && images.length == 0 && !fetching">%i18n:@no-image%</p> | ||||
| 		<div ref="slideA" class="slide a"></div> | ||||
| 		<div ref="slideB" class="slide b"></div> | ||||
| 	</div> | ||||
| @@ -72,7 +72,7 @@ export default define({ | ||||
| 			if (this.images.length == 0) return; | ||||
|  | ||||
| 			const index = Math.floor(Math.random() * this.images.length); | ||||
| 			const img = `url(${ this.images[index].url }?thumbnail&size=1024)`; | ||||
| 			const img = `url(${ this.images[index].url })`; | ||||
|  | ||||
| 			(this.$refs.slideB as any).style.backgroundImage = img; | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,8 @@ declare const _DOCS_URL_: string; | ||||
| declare const _STATS_URL_: string; | ||||
| declare const _STATUS_URL_: string; | ||||
| declare const _DEV_URL_: string; | ||||
| declare const _REPOSITORY_URL_: string; | ||||
| declare const _FEEDBACK_URL_: string; | ||||
| declare const _LANG_: string; | ||||
| declare const _LANGS_: string; | ||||
| declare const _RECAPTCHA_SITEKEY_: string; | ||||
| @@ -32,6 +34,8 @@ export const docsUrl = _DOCS_URL_; | ||||
| export const statsUrl = _STATS_URL_; | ||||
| export const statusUrl = _STATUS_URL_; | ||||
| export const devUrl = _DEV_URL_; | ||||
| export const repositoryUrl = _REPOSITORY_URL_; | ||||
| export const feedbackUrl = _FEEDBACK_URL_; | ||||
| export const lang = _LANG_; | ||||
| export const langs = _LANGS_; | ||||
| export const recaptchaSitekey = _RECAPTCHA_SITEKEY_; | ||||
|   | ||||
| Before Width: | Height: | Size: 401 KiB After Width: | Height: | Size: 400 KiB | 
| Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 424 B | 
| @@ -35,10 +35,7 @@ import Vue from 'vue'; | ||||
| const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; | ||||
|  | ||||
| function isLeapYear(year) { | ||||
| 	return (year % 400 == 0) ? true : | ||||
| 		(year % 100 == 0) ? false : | ||||
| 			(year % 4 == 0) ? true : | ||||
| 				false; | ||||
| 	return !(year & (year % 25 ? 3 : 15)); | ||||
| } | ||||
|  | ||||
| export default Vue.extend({ | ||||
|   | ||||
| @@ -28,7 +28,7 @@ export default Vue.extend({ | ||||
| 			default: false | ||||
| 		}, | ||||
| 		title: { | ||||
| 			default: '%fa:R file%%i18n:@choose-prompt%s' | ||||
| 			default: '%fa:R file%%i18n:@choose-prompt%' | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
| <div class="root file" | ||||
| <div class="gvfdktuvdgwhmztnuekzkswkjygptfcv" | ||||
| 	:data-is-selected="isSelected" | ||||
| 	:data-is-contextmenu-showing="isContextmenuShowing" | ||||
| 	@click="onClick" | ||||
| @@ -16,7 +16,7 @@ | ||||
| 		<p>%i18n:@banner%</p> | ||||
| 	</div> | ||||
| 	<div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`"> | ||||
| 		<img :src="`${file.url}?thumbnail&size=128`" alt="" @load="onThumbnailLoaded"/> | ||||
| 		<img :src="file.url" alt="" @load="onThumbnailLoaded"/> | ||||
| 	</div> | ||||
| 	<p class="name"> | ||||
| 		<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> | ||||
| @@ -68,6 +68,11 @@ export default Vue.extend({ | ||||
| 				icon: '%fa:i-cursor%', | ||||
| 				action: this.rename | ||||
| 			}, { | ||||
| 				type: 'item', | ||||
| 				text: this.file.isSensitive ? '%i18n:@contextmenu.unmark-as-sensitive%' : '%i18n:@contextmenu.mark-as-sensitive%', | ||||
| 				icon: this.file.isSensitive ? '%fa:R eye%' : '%fa:R eye-slash%', | ||||
| 				action: this.toggleSensitive | ||||
| 			}, null, { | ||||
| 				type: 'item', | ||||
| 				text: '%i18n:@contextmenu.copy-url%', | ||||
| 				icon: '%fa:link%', | ||||
| @@ -149,6 +154,13 @@ export default Vue.extend({ | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		toggleSensitive() { | ||||
| 			(this as any).api('drive/files/update', { | ||||
| 				fileId: this.file.id, | ||||
| 				isSensitive: !this.file.isSensitive | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		copyUrl() { | ||||
| 			copyToClipboard(this.file.url); | ||||
| 			(this as any).apis.dialog({ | ||||
| @@ -312,10 +324,10 @@ root(isDark) | ||||
| 		> .ext | ||||
| 			opacity 0.5 | ||||
|  | ||||
| .root.file[data-darkmode] | ||||
| .gvfdktuvdgwhmztnuekzkswkjygptfcv[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .root.file:not([data-darkmode]) | ||||
| .gvfdktuvdgwhmztnuekzkswkjygptfcv:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
| <div class="root folder" | ||||
| <div class="ynntpczxvnusfwdyxsfuhvcmuypqopdd" | ||||
| 	:data-is-contextmenu-showing="isContextmenuShowing" | ||||
| 	:data-draghover="draghover" | ||||
| 	@click="onClick" | ||||
| @@ -216,10 +216,10 @@ export default Vue.extend({ | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| .root.folder | ||||
| root(isDark) | ||||
| 	padding 8px | ||||
| 	height 64px | ||||
| 	background lighten($theme-color, 95%) | ||||
| 	background isDark ? rgba($theme-color, 0.2) : lighten($theme-color, 95%) | ||||
| 	border-radius 4px | ||||
|  | ||||
| 	&, * | ||||
| @@ -229,10 +229,10 @@ export default Vue.extend({ | ||||
| 		pointer-events none | ||||
|  | ||||
| 	&:hover | ||||
| 		background lighten($theme-color, 90%) | ||||
| 		background isDark ? rgba(lighten($theme-color, 10%), 0.2) : lighten($theme-color, 90%) | ||||
|  | ||||
| 	&:active | ||||
| 		background lighten($theme-color, 85%) | ||||
| 		background isDark ? rgba(darken($theme-color, 10%), 0.2) : lighten($theme-color, 85%) | ||||
|  | ||||
| 	&[data-is-contextmenu-showing] | ||||
| 	&[data-draghover] | ||||
| @@ -248,16 +248,22 @@ export default Vue.extend({ | ||||
| 			border-radius 4px | ||||
|  | ||||
| 	&[data-draghover] | ||||
| 		background lighten($theme-color, 90%) | ||||
| 		background isDark ? rgba(darken($theme-color, 10%), 0.2) : lighten($theme-color, 90%) | ||||
|  | ||||
| 	> .name | ||||
| 		margin 0 | ||||
| 		font-size 0.9em | ||||
| 		color darken($theme-color, 30%) | ||||
| 		color isDark ? #fff : darken($theme-color, 30%) | ||||
|  | ||||
| 		> [data-fa] | ||||
| 			margin-right 4px | ||||
| 			margin-left 2px | ||||
| 			text-align left | ||||
|  | ||||
| .ynntpczxvnusfwdyxsfuhvcmuypqopdd[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .ynntpczxvnusfwdyxsfuhvcmuypqopdd:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -10,7 +10,10 @@ | ||||
| 			<span class="separator" v-if="folder != null">%fa:angle-right%</span> | ||||
| 			<span class="folder current" v-if="folder != null">{{ folder.name }}</span> | ||||
| 		</div> | ||||
| 		<input class="search" type="search" placeholder=" %i18n:@search%"/> | ||||
| 		<!-- | ||||
| 			TODO: #343 | ||||
| 			<input class="search" type="search" placeholder=" %i18n:@search%"/> | ||||
| 		--> | ||||
| 	</nav> | ||||
| 	<div class="main" :class="{ uploading: uploadings.length > 0, fetching }" | ||||
| 		ref="main" | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <mk-window width="400px" height="550px" @closed="$destroy"> | ||||
| 	<span slot="header" :class="$style.header"> | ||||
| 		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }} | ||||
| 		<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }} | ||||
| 	</span> | ||||
| 	<mk-followers :user="user"/> | ||||
| </mk-window> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <mk-window width="400px" height="550px" @closed="$destroy"> | ||||
| 	<span slot="header" :class="$style.header"> | ||||
| 		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }} | ||||
| 		<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }} | ||||
| 	</span> | ||||
| 	<mk-following :user="user"/> | ||||
| </mk-window> | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
| 			</div> | ||||
| 			<div class="trash"> | ||||
| 				<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable> | ||||
| 				<p>ゴミ箱</p> | ||||
| 				<p>%i18n:common.trash%</p> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| @@ -53,7 +53,7 @@ | ||||
| 				</div> | ||||
| 			</x-draggable> | ||||
| 			<div class="main"> | ||||
| 				<a @click="hint">カスタマイズのヒント</a> | ||||
| 				<a @click="hint">%i18n:common.customization-tips.title%</a> | ||||
| 				<div> | ||||
| 					<mk-post-form v-if="$store.state.settings.showPostFormOnTopOfTl"/> | ||||
| 					<mk-timeline ref="tl" @loaded="onTlLoaded"/> | ||||
| @@ -187,13 +187,13 @@ export default Vue.extend({ | ||||
| 	methods: { | ||||
| 		hint() { | ||||
| 			(this as any).apis.dialog({ | ||||
| 				title: '%fa:info-circle%カスタマイズのヒント', | ||||
| 				text: '<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' + | ||||
| 					'<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' + | ||||
| 					'<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' + | ||||
| 					'<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>', | ||||
| 				title: '%fa:info-circle%%i18n:common.customization-tips.title%', | ||||
| 				text: '<p>%i18n:common.customization-tips.paragraph1%</p>' + | ||||
| 					'<p>%i18n:common.customization-tips.paragraph2%</p>' + | ||||
| 					'<p>%i18n:common.customization-tips.paragraph3%</p>' + | ||||
| 					'<p>%i18n:common.customization-tips.paragraph4%</p>', | ||||
| 				actions: [{ | ||||
| 					text: 'Got it!' | ||||
| 					text: '%i18n:common.customization-tips.gotit%' | ||||
| 				}] | ||||
| 			}); | ||||
| 		}, | ||||
|   | ||||
| @@ -1,5 +1,11 @@ | ||||
| <template> | ||||
| <a class="mk-media-image" | ||||
| <div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide" @click="hide = false"> | ||||
| 	<div> | ||||
| 		<b>%fa:exclamation-triangle% %i18n:@sensitive%</b> | ||||
| 		<span>%i18n:@click-to-show%</span> | ||||
| 	</div> | ||||
| </div> | ||||
| <a class="lcjomzwbohoelkxsnuqjiaccdbdfiazy" v-else | ||||
| 	:href="image.url" | ||||
| 	@mousemove="onMousemove" | ||||
| 	@mouseleave="onMouseleave" | ||||
| @@ -21,13 +27,17 @@ export default Vue.extend({ | ||||
| 		}, | ||||
| 		raw: { | ||||
| 			default: false | ||||
| 		}, | ||||
| 		hide: { | ||||
| 			type: Boolean, | ||||
| 			default: true | ||||
| 		} | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		style(): any { | ||||
| 			return { | ||||
| 				'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent', | ||||
| 				'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)` | ||||
| 				'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url})` | ||||
| 			}; | ||||
| 		} | ||||
| 	}, | ||||
| @@ -56,16 +66,30 @@ export default Vue.extend({ | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .mk-media-image | ||||
| .lcjomzwbohoelkxsnuqjiaccdbdfiazy | ||||
| 	display block | ||||
| 	cursor zoom-in | ||||
| 	overflow hidden | ||||
| 	width 100% | ||||
| 	height 100% | ||||
| 	background-position center | ||||
| 	border-radius 4px | ||||
|  | ||||
| 	&:not(:hover) | ||||
| 		background-size cover | ||||
|  | ||||
| .ldwbgwstjsdgcjruamauqdrffetqudry | ||||
| 	display flex | ||||
| 	justify-content center | ||||
| 	align-items center | ||||
| 	background #111 | ||||
| 	color #fff | ||||
|  | ||||
| 	> div | ||||
| 		display table-cell | ||||
| 		text-align center | ||||
| 		font-size 12px | ||||
|  | ||||
| 		> b | ||||
| 			display block | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -1,12 +1,19 @@ | ||||
| <template> | ||||
| 	<video class="mk-media-video" | ||||
| <div class="uofhebxjdgksfmltszlxurtjnjjsvioh" v-if="video.isSensitive && hide" @click="hide = false"> | ||||
| 	<div> | ||||
| 		<b>%fa:exclamation-triangle% %i18n:@sensitive%</b> | ||||
| 		<span>%i18n:@click-to-show%</span> | ||||
| 	</div> | ||||
| </div> | ||||
| <div class="vwxdhznewyashiknzolsoihtlpicqepe" v-else> | ||||
| 	<video class="video" | ||||
| 		:src="video.url" | ||||
| 		:title="video.name" | ||||
| 		controls | ||||
| 		@dblclick.prevent="onClick" | ||||
| 		ref="video" | ||||
| 		v-if="inlinePlayable" /> | ||||
| 	<a class="mk-media-video-thumbnail" | ||||
| 	<a class="thumbnail" | ||||
| 		:href="video.url" | ||||
| 		:style="imageStyle" | ||||
| 		@click.prevent="onClick" | ||||
| @@ -14,6 +21,7 @@ | ||||
| 		v-else> | ||||
| 		%fa:R play-circle% | ||||
| 	</a> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| @@ -21,11 +29,23 @@ import Vue from 'vue'; | ||||
| import MkMediaVideoDialog from './media-video-dialog.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: ['video', 'inlinePlayable'], | ||||
| 	props: { | ||||
| 		video: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		inlinePlayable: { | ||||
| 			default: false | ||||
| 		}, | ||||
| 		hide: { | ||||
| 			type: Boolean, | ||||
| 			default: true | ||||
| 		} | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		imageStyle(): any { | ||||
| 			return { | ||||
| 				'background-image': `url(${this.video.url}?thumbnail&size=512)` | ||||
| 				'background-image': `url(${this.video.url})` | ||||
| 			}; | ||||
| 		} | ||||
| 	}, | ||||
| @@ -47,22 +67,39 @@ export default Vue.extend({ | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .mk-media-video | ||||
| 	display block | ||||
| 	width 100% | ||||
| 	height 100% | ||||
| 	border-radius 4px | ||||
| .vwxdhznewyashiknzolsoihtlpicqepe | ||||
| 	.video | ||||
| 		display block | ||||
| 		width 100% | ||||
| 		height 100% | ||||
| 		border-radius 4px | ||||
|  | ||||
| .mk-media-video-thumbnail | ||||
| 	.thumbnail | ||||
| 		display flex | ||||
| 		justify-content center | ||||
| 		align-items center | ||||
| 		font-size 3.5em | ||||
|  | ||||
| 		cursor zoom-in | ||||
| 		overflow hidden | ||||
| 		background-position center | ||||
| 		background-size cover | ||||
| 		width 100% | ||||
| 		height 100% | ||||
|  | ||||
| .uofhebxjdgksfmltszlxurtjnjjsvioh | ||||
| 	display flex | ||||
| 	justify-content center | ||||
| 	align-items center | ||||
| 	font-size 3.5em | ||||
| 	background #111 | ||||
| 	color #fff | ||||
|  | ||||
| 	> div | ||||
| 		display table-cell | ||||
| 		text-align center | ||||
| 		font-size 12px | ||||
|  | ||||
| 		> b | ||||
| 			display block | ||||
|  | ||||
| 	cursor zoom-in | ||||
| 	overflow hidden | ||||
| 	background-position center | ||||
| 	background-size cover | ||||
| 	width 100% | ||||
| 	height 100% | ||||
| </style> | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { url } from '../../../config'; | ||||
| import getAcct from '../../../../../acct/render'; | ||||
| import getAcct from '../../../../../misc/acct/render'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: ['user'], | ||||
|   | ||||
| @@ -46,7 +46,7 @@ | ||||
| 				<mk-media-list :media-list="p.media" :raw="true"/> | ||||
| 			</div> | ||||
| 			<mk-poll v-if="p.poll" :note="p"/> | ||||
| 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/> | ||||
| 			<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> | ||||
| 			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | ||||
| 			<div class="map" v-if="p.geo" ref="map"></div> | ||||
| 			<div class="renote" v-if="p.renote"> | ||||
|   | ||||
| @@ -56,10 +56,10 @@ | ||||
| 				<button @click="menu" ref="menuButton"> | ||||
| 					%fa:ellipsis-h% | ||||
| 				</button> | ||||
| 				<button title="%i18n:@detail"> | ||||
| 				<!-- <button title="%i18n:@detail"> | ||||
| 					<template v-if="!isDetailOpened">%fa:caret-down%</template> | ||||
| 					<template v-if="isDetailOpened">%fa:caret-up%</template> | ||||
| 				</button> | ||||
| 				</button> --> | ||||
| 			</footer> | ||||
| 		</div> | ||||
| 	</article> | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { url } from '../../../config'; | ||||
| import getNoteSummary from '../../../../../renderers/get-note-summary'; | ||||
| import getNoteSummary from '../../../../../misc/get-note-summary'; | ||||
|  | ||||
| import XNote from './notes.note.vue'; | ||||
|  | ||||
|   | ||||
| @@ -110,7 +110,7 @@ | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import getNoteSummary from '../../../../../renderers/get-note-summary'; | ||||
| import getNoteSummary from '../../../../../misc/get-note-summary'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
|   | ||||
| @@ -8,7 +8,11 @@ | ||||
| 	<div class="content"> | ||||
| 		<div v-if="visibility == 'specified'" class="visibleUsers"> | ||||
| 			<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span> | ||||
| 			<a @click="addVisibleUser">+ユーザーを追加</a> | ||||
| 			<a @click="addVisibleUser">%i18n:@add-visible-user%</a> | ||||
| 		</div> | ||||
| 		<div class="hashtags" v-if="recentHashtags.length > 0"> | ||||
| 			<b>%i18n:@recent-tags%:</b> | ||||
| 			<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" title="%@click-to-tagging%">#{{ tag }}</a> | ||||
| 		</div> | ||||
| 		<input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)"> | ||||
| 		<textarea :class="{ with: (files.length != 0 || poll) }" | ||||
| @@ -19,7 +23,7 @@ | ||||
| 		<div class="medias" :class="{ with: poll }" v-show="files.length != 0"> | ||||
| 			<x-draggable :list="files" :options="{ animation: 150 }"> | ||||
| 				<div v-for="file in files" :key="file.id"> | ||||
| 					<div class="img" :style="{ backgroundImage: `url(${file.url}?thumbnail&size=64)` }" :title="file.name"></div> | ||||
| 					<div class="img" :style="{ backgroundImage: `url(${file.url})` }" :title="file.name"></div> | ||||
| 					<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" title="%i18n:@attach-cancel%" alt=""/> | ||||
| 				</div> | ||||
| 			</x-draggable> | ||||
| @@ -32,9 +36,15 @@ | ||||
| 	<button class="drive" title="%i18n:@attach-media-from-drive%" @click="chooseFileFromDrive">%fa:cloud%</button> | ||||
| 	<button class="kao" title="%i18n:@insert-a-kao%" @click="kao">%fa:R smile%</button> | ||||
| 	<button class="poll" title="%i18n:@create-poll%" @click="poll = true">%fa:chart-pie%</button> | ||||
| 	<button class="poll" title="内容を隠す" @click="useCw = !useCw">%fa:eye-slash%</button> | ||||
| 	<button class="geo" title="位置情報を添付する" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> | ||||
| 	<button class="visibility" title="公開範囲" @click="setVisibility" ref="visibilityButton">%fa:lock%</button> | ||||
| 	<button class="poll" title="%i18n:@hide-contents%" @click="useCw = !useCw">%fa:eye-slash%</button> | ||||
| 	<button class="geo" title="%i18n:@attach-location-information%" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> | ||||
| 	<button class="visibility" title="%i18n:@visibility%" @click="setVisibility" ref="visibilityButton"> | ||||
| 		<span v-if="visibility === 'public'">%fa:globe%</span> | ||||
| 		<span v-if="visibility === 'home'">%fa:home%</span> | ||||
| 		<span v-if="visibility === 'followers'">%fa:unlock%</span> | ||||
| 		<span v-if="visibility === 'specified'">%fa:envelope%</span> | ||||
| 		<span v-if="visibility === 'private'">%fa:lock%</span> | ||||
| 	</button> | ||||
| 	<p class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</p> | ||||
| 	<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post"> | ||||
| 		{{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/> | ||||
| @@ -46,6 +56,7 @@ | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import insertTextAtCursor from 'insert-text-at-cursor'; | ||||
| import * as XDraggable from 'vuedraggable'; | ||||
| import getKao from '../../../common/scripts/get-kao'; | ||||
| import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; | ||||
| @@ -91,7 +102,8 @@ export default Vue.extend({ | ||||
| 			visibility: 'public', | ||||
| 			visibleUsers: [], | ||||
| 			autocomplete: null, | ||||
| 			draghover: false | ||||
| 			draghover: false, | ||||
| 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]') | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| @@ -131,7 +143,9 @@ export default Vue.extend({ | ||||
| 		}, | ||||
|  | ||||
| 		canPost(): boolean { | ||||
| 			return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.renote); | ||||
| 			return !this.posting && | ||||
| 				(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) && | ||||
| 				(this.text.trim().length <= 1000); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| @@ -183,6 +197,10 @@ export default Vue.extend({ | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		addTag(tag: string) { | ||||
| 			insertTextAtCursor(this.$refs.text, ` #${tag} `); | ||||
| 		}, | ||||
|  | ||||
| 		watch() { | ||||
| 			this.$watch('text', () => this.saveDraft()); | ||||
| 			this.$watch('poll', () => this.saveDraft()); | ||||
| @@ -235,7 +253,7 @@ export default Vue.extend({ | ||||
| 		}, | ||||
|  | ||||
| 		onKeydown(e) { | ||||
| 			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); | ||||
| 			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post(); | ||||
| 		}, | ||||
|  | ||||
| 		onPaste(e) { | ||||
| @@ -287,7 +305,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 		setGeo() { | ||||
| 			if (navigator.geolocation == null) { | ||||
| 				alert('お使いの端末は位置情報に対応していません'); | ||||
| 				alert('%i18n:@geolocation-alert%'); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| @@ -295,10 +313,10 @@ export default Vue.extend({ | ||||
| 				this.geo = pos.coords; | ||||
| 				this.$emit('geo-attached', this.geo); | ||||
| 			}, err => { | ||||
| 				alert('エラー: ' + err.message); | ||||
| 				alert('%i18n:@error%: ' + err.message); | ||||
| 			}, { | ||||
| 				enableHighAccuracy: true | ||||
| 			}); | ||||
| 					enableHighAccuracy: true | ||||
| 				}); | ||||
| 		}, | ||||
|  | ||||
| 		removeGeo() { | ||||
| @@ -318,7 +336,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 		addVisibleUser() { | ||||
| 			(this as any).apis.input({ | ||||
| 				title: 'ユーザー名を入力してください' | ||||
| 				title: '%i18n:@enter-username%' | ||||
| 			}).then(username => { | ||||
| 				(this as any).api('users/show', { | ||||
| 					username | ||||
| @@ -370,6 +388,12 @@ export default Vue.extend({ | ||||
| 			}).then(() => { | ||||
| 				this.posting = false; | ||||
| 			}); | ||||
|  | ||||
| 			if (this.text && this.text != '') { | ||||
| 				const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag); | ||||
| 				const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; | ||||
| 				localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], []))); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		saveDraft() { | ||||
| @@ -452,7 +476,7 @@ root(isDark) | ||||
| 			margin 0 | ||||
| 			max-width 100% | ||||
| 			min-width 100% | ||||
| 			min-height 64px | ||||
| 			min-height 84px | ||||
|  | ||||
| 			&:hover | ||||
| 				& + * | ||||
| @@ -478,6 +502,19 @@ root(isDark) | ||||
| 				margin-right 16px | ||||
| 				color isDark ? #fff : #666 | ||||
|  | ||||
| 		> .hashtags | ||||
| 			margin 0 0 8px 0 | ||||
| 			overflow hidden | ||||
| 			white-space nowrap | ||||
| 			font-size 14px | ||||
|  | ||||
| 			> b | ||||
| 				color isDark ? #9baec8 : darken($theme-color, 20%) | ||||
|  | ||||
| 			> * | ||||
| 				margin-right 8px | ||||
| 				white-space nowrap | ||||
|  | ||||
| 		> .medias | ||||
| 			margin 0 | ||||
| 			padding 0 | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| <div class="profile"> | ||||
| 	<label class="avatar ui from group"> | ||||
| 		<p>%i18n:@avatar%</p> | ||||
| 		<img class="avatar" :src="`${$store.state.i.avatarUrl}?thumbnail&size=64`" alt="avatar"/> | ||||
| 		<img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/> | ||||
| 		<button class="ui" @click="updateAvatar">%i18n:@choice-avatar%</button> | ||||
| 	</label> | ||||
| 	<label class="ui from group"> | ||||
| @@ -63,7 +63,7 @@ export default Vue.extend({ | ||||
| 				description: this.description || null, | ||||
| 				birthday: this.birthday || null | ||||
| 			}).then(() => { | ||||
| 				(this as any).apis.notify('プロフィールを更新しました'); | ||||
| 				(this as any).apis.notify('%i18n:@profile-updated%'); | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeIsLocked() { | ||||
|   | ||||
| @@ -410,7 +410,7 @@ export default Vue.extend({ | ||||
| 			localStorage.clear(); | ||||
| 			(this as any).apis.dialog({ | ||||
| 				title: '%i18n:@cache-cleared%', | ||||
| 				text: '%i18n:@caache-cleared-desc%' | ||||
| 				text: '%i18n:@cache-cleared-desc%' | ||||
| 			}); | ||||
| 		}, | ||||
| 		soundTest() { | ||||
|   | ||||
 syuilo
					syuilo