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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								.npmrc
									
										
									
									
									
								
							
							
						
						| 
						 | 
				
			
			@ -1,2 +1,2 @@
 | 
			
		|||
package-lock = false
 | 
			
		||||
save-exact=true
 | 
			
		||||
package-lock = false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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 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() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||