Merge branch 'master' into l10n_master

This commit is contained in:
syuilo 2018-08-01 08:34:22 +09:00 committed by GitHub
commit 77faf7a84c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
417 changed files with 5956 additions and 17208 deletions

View file

@ -1,67 +1,136 @@
# インスタンス名 name: example-instance-name # Name of your instance
name: description: example-description # Description of your instance
# インスタンスの紹介
description:
# サーバーのメンテナ情報
maintainer: maintainer:
# メンテナの名前 name: example-maitainer-name # Your name
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 and Port settings overview
url: # 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: http://localhost/
url:
# 待受ポート # A port that your Misskey server should listen.
port: # This value is not a port to use when accessing with a browser.
port: 80
# TLSの設定(利用しない場合は省略してください)
https:
# 証明書のパス...
key:
cert:
# MongoDBの設定
mongodb: mongodb:
host: localhost host: localhost
port: 27017 port: 27017
db: misskey db: misskey
user: user: example-misskey-user
pass: pass: example-misskey-pass
# Redisの設定
redis: redis:
host: localhost host: localhost
port: 6379 port: 6379
pass: pass: example-pass
# reCAPTCHAの設定 # Drive capacity of a local user (MB)
recaptcha: localDriveCapacityMb: 256
site_key:
secret_key:
# ServiceWrokerの設定 # Drive capacity of a remote user (MB)
sw: remoteDriveCapacityMb: 8
# VAPIDの公開鍵
public_key:
# VAPIDの秘密鍵 # If enabled:
private_key: # Server will not cache remote files (Using direct link instead).
# You can save your storage.
# Google Maps API # Users cannot see remote images when they turn off "Show media from a remote server" setting.
google_maps_api_key:
# Twitterインテグレーションの設定(利用しない場合は省略可能)
twitter:
# インテグレーション用アプリのコンシューマーキー
consumer_key:
# インテグレーション用アプリのコンシューマーシークレット
consumer_secret:
# true にすると、リモートのファイルをキャッシュしなくなります(直リンクします)。
# ストレージ容量を節約することができますが、「リモートメディアを表示しない」設定をオンにしているユーザーは、リモートの画像などは見えなくなります。
preventCache: false 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
View file

@ -2,3 +2,4 @@
*.psd -diff -text *.psd -diff -text
*.ai -diff -text *.ai -diff -text
yarn.lock -diff -text yarn.lock -diff -text
package-lock.json -diff -text

View file

@ -1,7 +0,0 @@
<!--
Misskeyへの貢献ありがとうございます。
バグの報告や提案などで、可能であれば以下の情報を含めてください。
* お使いのブラウザ
* デスクトップ版Misskeyかモバイル版Misskeyか
-->

22
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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 -->

View 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
View file

@ -10,5 +10,4 @@ npm-debug.log
*.pem *.pem
run.bat run.bat
api-docs.json api-docs.json
package-lock.json
*.log *.log

2
.npmrc
View file

@ -1,2 +1,2 @@
package-lock = false
save-exact=true save-exact=true
package-lock = false

12
.vscode/extensions.json vendored Normal file
View 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"
]
}

View file

@ -5,6 +5,15 @@ ChangeLog
This document describes breaking changes only. 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 4.0.0
----- -----

View file

@ -7,7 +7,7 @@
[![][dependencies-badge]][dependencies-link] [![][dependencies-badge]][dependencies-link]
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![Greenkeeper badge](https://badges.greenkeeper.io/syuilo/misskey.svg)](https://greenkeeper.io/) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![Greenkeeper badge](https://badges.greenkeeper.io/syuilo/misskey.svg)](https://greenkeeper.io/)
> Lead Maintainer: [syuilo][syuilo-link] **Microblogging. Redefined.**
**[Misskey](https://misskey.xyz)** is a completely open source, **[Misskey](https://misskey.xyz)** is a completely open source,
ultimately sophisticated professional microblogging software. ultimately sophisticated professional microblogging software.
@ -18,14 +18,13 @@ ultimately sophisticated professional microblogging software.
:sparkles: Features :sparkles: Features
---------------------------------------------------------------- ----------------------------------------------------------------
* Rich text contents
* Reactions * Reactions
* User lists * User lists
* Customizable column view (known as MisskeyDeck) * Customizable column view (called MisskeyDeck)
* and widgets! * and widgets!
* Private messages * Private messages
* Mute * ActivityPub support
* Real-time timelines
* ActivityPub compatible
and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz). 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 :heart: Backers & Sponsors
---------------------------------------------------------------- ----------------------------------------------------------------
| ![][nagarus-icon] | ![][dansup-icon] | | <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"> |
|:-:|:-:| |:-:|:-:|:-:|:-:|
| [nagarus][nagarus-link] | [dansup][dansup-link] | | [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 :four_leaf_clover: Copyright
---------------------------------------------------------------- ----------------------------------------------------------------
> Copyright (c) 2014-2018 syuilo > 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] [![][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-link]: https://syuilo.com
[syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70 [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

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -9,7 +9,7 @@ const q = {
'metadata._user.host': { 'metadata._user.host': {
$ne: null $ne: null
}, },
'metadata.isMetaOnly': false 'metadata.withoutChunks': false
}; };
async function main() { async function main() {
@ -57,7 +57,7 @@ async function main() {
DriveFile.update({ _id: file._id }, { DriveFile.update({ _id: file._id }, {
$set: { $set: {
'metadata.isMetaOnly': true 'metadata.withoutChunks': true
} }
}) })
]).then(async () => { ]).then(async () => {

View file

@ -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
View 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
View 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);
});

View file

@ -3,8 +3,8 @@
const chalk = require('chalk'); const chalk = require('chalk');
const sequential = require('promise-sequential'); const sequential = require('promise-sequential');
const { default: User } = require('../built/models/user'); const { default: User } = require('../../built/models/user');
const { default: DriveFile } = require('../built/models/drive-file'); const { default: DriveFile } = require('../../built/models/drive-file');
async function main() { async function main() {
const promiseGens = []; const promiseGens = [];

View file

@ -3,8 +3,8 @@
const chalk = require('chalk'); const chalk = require('chalk');
const sequential = require('promise-sequential'); const sequential = require('promise-sequential');
const { default: User } = require('../built/models/user'); const { default: User } = require('../../built/models/user');
const { default: DriveFile } = require('../built/models/drive-file'); const { default: DriveFile } = require('../../built/models/drive-file');
async function main() { async function main() {
const promiseGens = []; const promiseGens = [];

9
cli/migration/5.0.0.js Normal file
View 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
View 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);
});

View file

@ -14,7 +14,7 @@ RUN pacman -S --noconfirm pacman
RUN pacman-db-upgrade RUN pacman-db-upgrade
RUN pacman -S --noconfirm archlinux-keyring RUN pacman -S --noconfirm archlinux-keyring
RUN pacman -Syyu --noconfirm 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 COPY misskey.sh /root/misskey.sh
RUN chmod u+x /root/misskey.sh RUN chmod u+x /root/misskey.sh

6
docs/README.md Normal file
View 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
View 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.

View file

@ -1,13 +1,46 @@
# 運営ガイド # 運営ガイド
## ジョブキューの状態を調べる ## ジョブキューの状態を調べる
Misskeyのディレクトリで: coming soon
## 管理者ユーザーを設定する
``` shell ``` shell
node_modules/kue/bin/kue-dashboard -p 3050 node cli/mark-admin (ユーザーID または ユーザー名)
```
## 'verified'ユーザーを設定する
``` shell
node cli/mark-verified (ユーザーID または ユーザー名)
``` ```
ポート3050にアクセスするとUIが表示されます
## ユーザーを凍結する ## ユーザーを凍結する
``` shell ``` 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
```
> ユーザーの大事なファイルを削除する可能性があるので、この操作を実行する前にユーザーに告知することをお勧めします。

View file

@ -8,18 +8,13 @@ This guide describes how to install and setup Misskey.
---------------------------------------------------------------- ----------------------------------------------------------------
*1.* reCAPTCHA tokens *1.* Create Misskey user
---------------------------------------------------------------- ----------------------------------------------------------------
Misskey requires reCAPTCHA tokens. Running misskey on root is not a good idea so we create a user for that.
Please visit https://www.google.com/recaptcha/intro/ and generate keys. In debian for exemple :
*(optional)* Generating VAPID keys ```
---------------------------------------------------------------- adduser --disabled-password --disabled-login misskey
If you want to enable ServiceWroker, you need to generate VAPID keys:
``` shell
npm install web-push -g
web-push generate-vapid-keys
``` ```
*2.* Install dependencies *2.* Install dependencies
@ -27,25 +22,52 @@ web-push generate-vapid-keys
Please install and setup these softwares: Please install and setup these softwares:
#### Dependencies :package: #### Dependencies :package:
* *Node.js* and *npm* * **[Node.js](https://nodejs.org/en/)**
* **[MongoDB](https://www.mongodb.com/)** * **[MongoDB](https://www.mongodb.com/)** >= 3.6
* **[Redis](https://redis.io/)** * **[Redis](https://redis.io/)**
* **[ImageMagick](http://www.imagemagick.org/script/index.php)** >= 7.0
##### Optional ##### Optional
* [Elasticsearch](https://www.elastic.co/) - used to provide searching feature instead of MongoDB * [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: 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` 3. `node-gyp build`
4. `npm run build` 4. `npm run build`
*6.* That is it. *7.* That is it.
---------------------------------------------------------------- ----------------------------------------------------------------
Well done! Now, you have an environment that run to Misskey. Well done! Now, you have an environment that run to Misskey.
### Launch ### Launch normally
Just `sudo npm start`. GLHF! 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 ### Way to Update to latest version of your Misskey
1. `git reset --hard && git pull origin master` 1. `git fetch`
2. `npm install` 2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
3. `npm run build` 3. `npm install`
4. `npm run build`
----------------------------------------------------------------
If you have any questions or troubles, feel free to contact us!

View file

@ -8,10 +8,48 @@ Misskeyサーバーの構築にご関心をお寄せいただきありがとう
---------------------------------------------------------------- ----------------------------------------------------------------
*1.* reCAPTCHAトークンの用意 *1.* Misskeyユーザーの作成
---------------------------------------------------------------- ----------------------------------------------------------------
MisskeyはreCAPTCHAトークンを必要とします。 Misskeyのrootで実行しない方がよいため、代わりにユーザーを作成します。
https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生成してください。 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キーペアの生成 *(オプション)* VAPIDキーペアの生成
---------------------------------------------------------------- ----------------------------------------------------------------
@ -22,56 +60,67 @@ npm install web-push -g
web-push generate-vapid-keys web-push generate-vapid-keys
``` ```
*2.* 依存関係をインストールする *5.* 設定ファイルを作成する
---------------------------------------------------------------- ----------------------------------------------------------------
これらのソフトウェアをインストール・設定してください: 1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする。
2. `default.yml` を編集する。
#### 依存関係 :package: *6.* Misskeyのビルド
* *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のインストール
---------------------------------------------------------------- ----------------------------------------------------------------
1. `git clone -b master git://github.com/syuilo/misskey.git`
2. `cd misskey`
3. `npm install`
*4.* 設定ファイルを用意する 次のコマンドでMisskeyをビルドしてください:
----------------------------------------------------------------
`npm run config`コマンドを利用して、ガイドに従って情報を入力してください。
*5.* Misskeyのビルド `npm run build`
----------------------------------------------------------------
Debianをお使いであれば、`build-essential`パッケージをインストールする必要があります。
何らかのモジュールでエラーが発生する場合はnode-gypを使ってください:
1. `npm install -g node-gyp` 1. `npm install -g node-gyp`
2. `node-gyp configure` 2. `node-gyp configure`
3. `node-gyp build` 3. `node-gyp build`
4. `npm run build` 4. `npm run build`
*6.* 以上です! *7.* 以上です!
---------------------------------------------------------------- ----------------------------------------------------------------
お疲れ様でした。これでMisskeyを動かす準備は整いました。 お疲れ様でした。これで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を最新バージョンにアップデートする方法: ### Misskeyを最新バージョンにアップデートする方法:
1. `git reset --hard && git pull origin master` 1. `git fetch`
2. `npm install` 2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
3. `npm run build` 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. 完了

View file

@ -4,19 +4,19 @@ Misskey's Translation
If you find an untranslated part on Misskey: 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`. - 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%`. 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. - 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%`. - 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. - 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`. - 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. 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: "Timeline"` to `locales/en.yml`, and `timeline: "タイムライン"` to `locales/ja.yml`. - For example, in this case we add timeline: `timeline: "タイムライン"` to `locales/ja.yml`.
5. And done 5. And done

View file

@ -11,12 +11,12 @@ Misskey内の未翻訳箇所を見つけたら
- `foo`は実際にはその場に適したわかりやすい(英語の)名前にしてください。 - `foo`は実際にはその場に適したわかりやすい(英語の)名前にしてください。
- 例えば未翻訳箇所が「タイムライン」というテキストだった場合、`%i18n:@timeline%`のようにします。 - 例えば未翻訳箇所が「タイムライン」というテキストだった場合、`%i18n:@timeline%`のようにします。
3. /locales 内にあるそれぞれの言語ファイルを開き、1.で見つけた<strong>ファイル名(パス)</strong>のキーが存在するか確認し、無ければ作成してください。 3. `locales/ja.yml`を開き、1.で見つけた<strong>ファイル名(パス)</strong>のキーが存在するか確認し、無ければ作成してください。
- パスの`src/client/app/`は省略してください。 - パスの`src/client/app/`は省略してください。
- 例えば、今回の例では`src/client/app/mobile/views/pages/home.vue`の未翻訳箇所を修正したいので、キーは`mobile/views/pages/home.vue`になります。 - 例えば、今回の例では`src/client/app/mobile/views/pages/home.vue`の未翻訳箇所を修正したいので、キーは`mobile/views/pages/home.vue`になります。
4. そのキーの直下に2.で置換した`foo`の部分をキーとし、翻訳後のテキストを値とするプロパティを追加します。 4. そのキーの直下に2.で置換した`foo`の部分をキーとし、テキストを値とするプロパティを追加します。
- 例えば、今回の例で言うと`locales/ja.yml`に`timeline: "タイムライン"`、`locales/en.yml`に`timeline: "Timeline"`を追加します。 - 例えば、今回の例で言うと`locales/ja.yml`に`timeline: "タイムライン"`を追加します。
5. 完了です! 5. 完了です!

View file

@ -1,6 +0,0 @@
How to create indexes
=====================
``` shell
curl -XPOST localhost:9200/misskey -d @path/to/mappings.json
```

View file

@ -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"
}
}
}
}
}

View file

@ -9,6 +9,7 @@ import * as ts from 'gulp-typescript';
const sourcemaps = require('gulp-sourcemaps'); const sourcemaps = require('gulp-sourcemaps');
import tslint from 'gulp-tslint'; import tslint from 'gulp-tslint';
const cssnano = require('gulp-cssnano'); const cssnano = require('gulp-cssnano');
const stylus = require('gulp-stylus');
import * as uglifyComposer from 'gulp-uglify/composer'; import * as uglifyComposer from 'gulp-uglify/composer';
import pug = require('gulp-pug'); import pug = require('gulp-pug');
import * as rimraf from 'rimraf'; import * as rimraf from 'rimraf';
@ -20,9 +21,8 @@ import * as replace from 'gulp-replace';
import * as htmlmin from 'gulp-htmlmin'; import * as htmlmin from 'gulp-htmlmin';
const uglifyes = require('uglify-es'); const uglifyes = require('uglify-es');
import locales from './locales'; const locales = require('./locales');
import { fa } from './src/build/fa'; import { fa } from './src/misc/fa';
const client = require('./built/client/meta.json');
import config from './src/config'; import config from './src/config';
const uglify = uglifyComposer(uglifyes, console); const uglify = uglifyComposer(uglifyes, console);
@ -38,8 +38,6 @@ if (isDebug) {
const constants = require('./src/const.json'); const constants = require('./src/const.json');
require('./src/client/docs/gulpfile.ts');
gulp.task('build', [ gulp.task('build', [
'build:ts', 'build:ts',
'build:copy', 'build:copy',
@ -47,8 +45,6 @@ gulp.task('build', [
'doc' 'doc'
]); ]);
gulp.task('rebuild', ['clean', 'build']);
gulp.task('build:ts', () => { gulp.task('build:ts', () => {
const tsProject = ts.createProject('./tsconfig.json'); const tsProject = ts.createProject('./tsconfig.json');
@ -85,7 +81,7 @@ gulp.task('lint', () =>
); );
gulp.task('format', () => gulp.task('format', () =>
gulp.src('./src/**/*.ts') gulp.src('./src/**/*.ts')
.pipe(tslint({ .pipe(tslint({
formatter: 'verbose', formatter: 'verbose',
fix: true fix: true
@ -94,10 +90,10 @@ gulp.src('./src/**/*.ts')
); );
gulp.task('mocha', () => gulp.task('mocha', () =>
gulp.src([]) gulp.src('./test/**/*.ts')
.pipe(mocha({ .pipe(mocha({
exit: true, exit: true,
compilers: 'ts:ts-node/register' require: 'ts-node/register'
} as any)) } as any))
); );
@ -118,8 +114,9 @@ gulp.task('build:client', [
'copy:client' 'copy:client'
]); ]);
gulp.task('build:client:script', () => gulp.task('build:client:script', () => {
gulp.src(['./src/client/app/boot.js', './src/client/app/safe.js']) 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('VERSION', JSON.stringify(client.version)))
.pipe(replace('API', JSON.stringify(config.api_url))) .pipe(replace('API', JSON.stringify(config.api_url)))
.pipe(replace('ENV', JSON.stringify(env))) .pipe(replace('ENV', JSON.stringify(env)))
@ -127,8 +124,8 @@ gulp.task('build:client:script', () =>
.pipe(isProduction ? uglify({ .pipe(isProduction ? uglify({
toplevel: true toplevel: true
} as any) : gutil.noop()) } as any) : gutil.noop())
.pipe(gulp.dest('./built/client/assets/')) as any .pipe(gulp.dest('./built/client/assets/'));
); });
gulp.task('build:client:styles', () => gulp.task('build:client:styles', () =>
gulp.src('./src/client/app/init.css') gulp.src('./src/client/app/init.css')
@ -201,3 +198,10 @@ gulp.task('build:client:pug', [
})) }))
.pipe(gulp.dest('./built/client/app/')) .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/'))
);

View file

@ -889,6 +889,7 @@ mobile/views/pages/settings/settings.profile.vue:
saved: "Profile updated" saved: "Profile updated"
uploading: "Uploading" uploading: "Uploading"
upload-failed: "Failed to upload" upload-failed: "Failed to upload"
mobile/views/pages/search.vue: mobile/views/pages/search.vue:
search: "Search" search: "Search"
empty: "No posts were found for '{}'" empty: "No posts were found for '{}'"

View file

@ -40,7 +40,7 @@ common:
hmm: "Hmm ... ?" hmm: "Hmm ... ?"
surprise: "Wow" surprise: "Wow"
congrats: "Félicitations !" congrats: "Félicitations !"
angry: "En colère" angry: "Faché"
confused: "Confus" confused: "Confus"
pudding: "Pudding" pudding: "Pudding"
note-placeholders: note-placeholders:

27
locales/index.js Normal file
View 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;

View file

@ -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;

View file

@ -7,6 +7,14 @@ common:
about-title: "A ⭐ of fediverse." about-title: "A ⭐ of fediverse."
about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。" 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: time:
unknown: "なぞのじかん" unknown: "なぞのじかん"
future: "未来" future: "未来"
@ -19,6 +27,8 @@ common:
months_ago: "{}ヶ月前" months_ago: "{}ヶ月前"
years_ago: "{}年前" years_ago: "{}年前"
trash: "ゴミ箱"
weekday-short: weekday-short:
sunday: "日" sunday: "日"
monday: "月" monday: "月"
@ -56,6 +66,7 @@ common:
my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。" my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。"
i-like-sushi: "私は(プリンよりむしろ)寿司が好き" i-like-sushi: "私は(プリンよりむしろ)寿司が好き"
show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示" show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示"
verified-user: "認証済みのユーザー"
reversi: reversi:
drawn: "引き分け" drawn: "引き分け"
@ -63,6 +74,7 @@ common:
opponent-turn: "相手のターンです" opponent-turn: "相手のターンです"
turn-of: "{}のターンです" turn-of: "{}のターンです"
past-turn-of: "{}のターン" past-turn-of: "{}のターン"
won: "{}の勝ち"
widgets: widgets:
analog-clock: "アナログ時計" analog-clock: "アナログ時計"
@ -93,6 +105,7 @@ common:
widgets: "ウィジェット" widgets: "ウィジェット"
home: "ホーム" home: "ホーム"
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
notifications: "通知" notifications: "通知"
list: "リスト" list: "リスト"
@ -280,6 +293,11 @@ common/views/widgets/memo.vue:
memo: "ここに書いて!" memo: "ここに書いて!"
save: "保存" save: "保存"
common/views/widgets/slideshow.vue:
folder-customize-mode: "フォルダを指定するには、カスタマイズモードを終了してください"
folder: "クリックしてフォルダを指定してください"
no-image: "このフォルダには画像がありません"
common/views/pages/follow.vue: common/views/pages/follow.vue:
signed-in-as: "{}としてサインイン中" signed-in-as: "{}としてサインイン中"
following: "フォロー中" following: "フォロー中"
@ -329,6 +347,8 @@ desktop/views/components/drive.file.vue:
banner: "バナー" banner: "バナー"
contextmenu: contextmenu:
rename: "名前を変更" rename: "名前を変更"
mark-as-sensitive: "閲覧注意に設定"
unmark-as-sensitive: "閲覧注意を解除"
copy-url: "URLをコピー" copy-url: "URLをコピー"
download: "ダウンロード" download: "ダウンロード"
else-files: "その他..." else-files: "その他..."
@ -376,6 +396,14 @@ desktop/views/components/drive.vue:
upload: "ファイルをアップロード" upload: "ファイルをアップロード"
url-upload: "URLからアップロード" 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: desktop/views/components/follow-button.vue:
following: "フォロー中" following: "フォロー中"
follow: "フォロー" follow: "フォロー"
@ -440,12 +468,16 @@ desktop/views/components/notes.note.vue:
desktop/views/components/notes.vue: desktop/views/components/notes.vue:
error: "読み込みに失敗しました。" error: "読み込みに失敗しました。"
retry: "リトライ" retry: "リトライ"
load-more: "もっと読み込む"
desktop/views/components/notifications.vue: desktop/views/components/notifications.vue:
more: "もっと見る" more: "もっと見る"
empty: "ありません!" empty: "ありません!"
desktop/views/components/post-form.vue: desktop/views/components/post-form.vue:
add-visible-user: "+ユーザーを追加"
attach-location-information: "位置情報を添付する"
hide-contents: "内容を隠す"
reply-placeholder: "この投稿への返信..." reply-placeholder: "この投稿への返信..."
quote-placeholder: "この投稿を引用..." quote-placeholder: "この投稿を引用..."
submit: "投稿" submit: "投稿"
@ -464,6 +496,12 @@ desktop/views/components/post-form.vue:
insert-a-kao: "v('ω')v" insert-a-kao: "v('ω')v"
create-poll: "アンケートを作成" create-poll: "アンケートを作成"
text-remain: "残り{}文字" text-remain: "残り{}文字"
recent-tags: "最近"
click-to-tagging: "クリックでタグ付け"
visibility: "公開範囲"
geolocation-alert: "お使いの端末は位置情報に対応していません"
error: "エラー"
enter-username: "ユーザー名を入力してください"
desktop/views/components/post-form-window.vue: desktop/views/components/post-form-window.vue:
note: "新規投稿" note: "新規投稿"
@ -512,6 +550,8 @@ desktop/views/components/settings.vue:
display: "デザインと表示" display: "デザインと表示"
customize: "ホームをカスタマイズ" customize: "ホームをカスタマイズ"
choose-wallpaper: "壁紙を選択"
delete-wallpaper: "壁紙を削除"
dark-mode: "ダークモード" dark-mode: "ダークモード"
circle-icons: "円形のアイコンを使用" circle-icons: "円形のアイコンを使用"
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用" gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
@ -621,8 +661,12 @@ desktop/views/components/settings.profile.vue:
description: "自己紹介" description: "自己紹介"
birthday: "誕生日" birthday: "誕生日"
save: "保存" save: "保存"
locked-account: "アカウントの保護"
is-locked: "投稿を非公開にする"
other: "その他"
is-bot: "このアカウントはBotです" is-bot: "このアカウントはBotです"
is-cat: "このアカウントはCatです" is-cat: "このアカウントはCatです"
profile-updated: "プロフィールを更新しました"
desktop/views/components/sub-note-content.vue: desktop/views/components/sub-note-content.vue:
private: "この投稿は非公開です" private: "この投稿は非公開です"
@ -636,6 +680,7 @@ desktop/views/components/taskmanager.vue:
desktop/views/components/timeline.vue: desktop/views/components/timeline.vue:
home: "ホーム" home: "ホーム"
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
list: "リスト" list: "リスト"
@ -648,7 +693,7 @@ desktop/views/components/ui.header.account.vue:
favorites: "お気に入り" favorites: "お気に入り"
lists: "リスト" lists: "リスト"
follow-requests: "フォロー申請" follow-requests: "フォロー申請"
customize: "カスタマイズ" customize: "ホームのカスタマイズ"
settings: "設定" settings: "設定"
signout: "サインアウト" signout: "サインアウト"
dark: "闇に飲まれる" dark: "闇に飲まれる"
@ -698,6 +743,7 @@ desktop/views/components/window.vue:
desktop/views/pages/deck/deck.tl-column.vue: desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ" is-media-only: "メディア投稿のみ"
is-media-view: "メディアビュー" is-media-view: "メディアビュー"
edit: "オプション"
desktop/views/pages/deck/deck.note.vue: desktop/views/pages/deck/deck.note.vue:
reposted-by: "{}がRenote" reposted-by: "{}がRenote"
@ -844,6 +890,14 @@ mobile/views/components/drive.file-detail.vue:
hash: "ハッシュ (md5)" hash: "ハッシュ (md5)"
exif: "EXIF" 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: mobile/views/components/follow-button.vue:
following: "フォロー中" following: "フォロー中"
follow: "フォロー" follow: "フォロー"
@ -958,6 +1012,7 @@ mobile/views/pages/following.vue:
mobile/views/pages/home.vue: mobile/views/pages/home.vue:
home: "ホーム" home: "ホーム"
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mobile/views/pages/messaging.vue: mobile/views/pages/messaging.vue:
@ -1088,11 +1143,17 @@ docs:
properties: "プロパティ" properties: "プロパティ"
endpoints: endpoints:
params: "パラメータ" params: "パラメータ"
no-params: "パラメータはありません"
res: "レスポンス" 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: props:
name: "名前" name: "名前"
type: "型" type: "型"
optional: "オプション"
description: "説明" description: "説明"
yes: "はい"
no: "いいえ"

View file

@ -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 が生成されます

View file

@ -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"

View file

@ -1,21 +1,18 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "4.15.0", "version": "5.8.0",
"clientVersion": "1.0.6878", "clientVersion": "1.0.7664",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,
"scripts": { "scripts": {
"config": "node ./cli/init.js",
"start": "node ./built", "start": "node ./built",
"debug": "DEBUG=misskey:* node ./built", "debug": "DEBUG=misskey:* node ./built",
"swagger": "node ./swagger.js",
"build": "webpack && gulp build", "build": "webpack && gulp build",
"webpack": "webpack", "webpack": "webpack",
"watch": "webpack --watch", "watch": "webpack --watch",
"gulp": "gulp build", "gulp": "gulp build",
"rebuild": "gulp rebuild",
"clean": "gulp clean", "clean": "gulp clean",
"cleanall": "gulp cleanall", "cleanall": "gulp cleanall",
"lint": "gulp lint", "lint": "gulp lint",
@ -27,15 +24,15 @@
"@fortawesome/fontawesome-free-brands": "5.0.13", "@fortawesome/fontawesome-free-brands": "5.0.13",
"@fortawesome/fontawesome-free-regular": "5.0.13", "@fortawesome/fontawesome-free-regular": "5.0.13",
"@fortawesome/fontawesome-free-solid": "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/rap": "0.1.2",
"@prezzemolo/zip": "0.0.3", "@prezzemolo/zip": "0.0.3",
"@types/bcryptjs": "2.4.1", "@types/bcryptjs": "2.4.1",
"@types/dateformat": "1.0.1",
"@types/debug": "0.0.30", "@types/debug": "0.0.30",
"@types/deep-equal": "1.0.1", "@types/deep-equal": "1.0.1",
"@types/elasticsearch": "5.0.24", "@types/elasticsearch": "5.0.25",
"@types/file-type": "5.2.1", "@types/file-type": "5.2.1",
"@types/gm": "1.18.0",
"@types/gulp": "3.8.36", "@types/gulp": "3.8.36",
"@types/gulp-htmlmin": "1.3.32", "@types/gulp-htmlmin": "1.3.32",
"@types/gulp-mocha": "0.0.32", "@types/gulp-mocha": "0.0.32",
@ -43,31 +40,29 @@
"@types/gulp-replace": "0.0.31", "@types/gulp-replace": "0.0.31",
"@types/gulp-uglify": "3.0.5", "@types/gulp-uglify": "3.0.5",
"@types/gulp-util": "3.0.34", "@types/gulp-util": "3.0.34",
"@types/inquirer": "0.0.42",
"@types/is-root": "1.0.0", "@types/is-root": "1.0.0",
"@types/is-url": "1.2.28", "@types/is-url": "1.2.28",
"@types/js-yaml": "3.11.1", "@types/js-yaml": "3.11.2",
"@types/jsdom": "11.0.6", "@types/jsdom": "11.0.6",
"@types/koa": "2.0.46", "@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-compress": "2.0.8",
"@types/koa-favicon": "2.0.19", "@types/koa-favicon": "2.0.19",
"@types/koa-logger": "3.1.0", "@types/koa-logger": "3.1.0",
"@types/koa-mount": "3.0.1", "@types/koa-mount": "3.0.1",
"@types/koa-multer": "1.0.0", "@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-send": "4.1.1",
"@types/koa-views": "2.0.3", "@types/koa-views": "2.0.3",
"@types/koa__cors": "2.2.2", "@types/koa__cors": "2.2.3",
"@types/kue": "0.11.9", "@types/minio": "6.0.2",
"@types/license-checker": "15.0.0",
"@types/mkdirp": "0.5.2", "@types/mkdirp": "0.5.2",
"@types/mocha": "5.2.3", "@types/mocha": "5.2.3",
"@types/mongodb": "3.1.0", "@types/mongodb": "3.1.2",
"@types/ms": "0.7.30", "@types/ms": "0.7.30",
"@types/node": "10.5.1", "@types/node": "10.5.4",
"@types/nopt": "3.0.29",
"@types/parse5": "5.0.0", "@types/parse5": "5.0.0",
"@types/portscanner": "2.1.0",
"@types/pug": "2.0.4", "@types/pug": "2.0.4",
"@types/qrcode": "1.2.0", "@types/qrcode": "1.2.0",
"@types/ratelimiter": "2.1.28", "@types/ratelimiter": "2.1.28",
@ -76,11 +71,14 @@
"@types/request-promise-native": "1.0.15", "@types/request-promise-native": "1.0.15",
"@types/rimraf": "2.0.2", "@types/rimraf": "2.0.2",
"@types/seedrandom": "2.4.27", "@types/seedrandom": "2.4.27",
"@types/sharp": "0.17.9",
"@types/showdown": "1.7.5",
"@types/single-line-log": "1.1.0", "@types/single-line-log": "1.1.0",
"@types/speakeasy": "2.0.2", "@types/speakeasy": "2.0.2",
"@types/systeminformation": "3.23.0",
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",
"@types/uuid": "3.4.3", "@types/uuid": "3.4.3",
"@types/webpack": "4.4.4", "@types/webpack": "4.4.8",
"@types/webpack-stream": "3.2.10", "@types/webpack-stream": "3.2.10",
"@types/websocket": "0.0.39", "@types/websocket": "0.0.39",
"@types/ws": "5.1.2", "@types/ws": "5.1.2",
@ -88,51 +86,54 @@
"autosize": "4.0.2", "autosize": "4.0.2",
"autwh": "0.1.0", "autwh": "0.1.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bee-queue": "1.2.2",
"bootstrap-vue": "2.0.0-rc.11", "bootstrap-vue": "2.0.0-rc.11",
"cafy": "8.0.0", "cafy": "11.3.0",
"chalk": "2.4.1", "chalk": "2.4.1",
"commander": "2.16.0",
"crc-32": "1.2.0", "crc-32": "1.2.0",
"css-loader": "0.28.11", "css-loader": "1.0.0",
"dateformat": "3.0.3",
"debug": "3.1.0", "debug": "3.1.0",
"deep-equal": "1.0.1", "deep-equal": "1.0.1",
"deepcopy": "0.6.3", "deepcopy": "0.6.3",
"diskusage": "0.2.4", "diskusage": "0.2.4",
"dompurify": "1.0.5", "dompurify": "1.0.5",
"elasticsearch": "15.0.0", "elasticsearch": "15.1.1",
"element-ui": "2.4.2", "element-ui": "2.4.5",
"emojilib": "2.2.12", "emojilib": "2.3.0",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"eslint": "5.0.1", "eslint": "5.0.1",
"eslint-plugin-vue": "4.5.0", "eslint-plugin-vue": "4.7.1",
"eventemitter3": "3.1.0", "eventemitter3": "3.1.0",
"exif-js": "2.3.0", "exif-js": "2.3.0",
"file-loader": "1.1.11", "file-loader": "1.1.11",
"file-type": "8.0.0", "file-type": "8.1.0",
"fuckadblock": "3.2.1", "fuckadblock": "3.2.1",
"gm": "1.23.1",
"gulp": "3.9.1", "gulp": "3.9.1",
"gulp-cssnano": "2.1.3", "gulp-cssnano": "2.1.3",
"gulp-htmlmin": "4.0.0", "gulp-htmlmin": "4.0.0",
"gulp-imagemin": "4.1.0", "gulp-imagemin": "4.1.0",
"gulp-mocha": "6.0.0", "gulp-mocha": "6.0.0",
"gulp-pug": "4.0.1", "gulp-pug": "4.0.1",
"gulp-rename": "1.3.0", "gulp-rename": "1.4.0",
"gulp-replace": "1.0.0", "gulp-replace": "1.0.0",
"gulp-sourcemaps": "2.6.4", "gulp-sourcemaps": "2.6.4",
"gulp-stylus": "2.7.0", "gulp-stylus": "2.7.0",
"gulp-tslint": "8.1.3", "gulp-tslint": "8.1.3",
"gulp-typescript": "4.0.2", "gulp-typescript": "4.0.2",
"gulp-uglify": "3.0.0", "gulp-uglify": "3.0.1",
"gulp-util": "3.0.8", "gulp-util": "3.0.8",
"hard-source-webpack-plugin": "0.10.1", "hard-source-webpack-plugin": "0.12.0",
"highlight.js": "9.12.0", "highlight.js": "9.12.0",
"html-minifier": "3.5.17", "html-minifier": "3.5.19",
"http-signature": "1.2.0", "http-signature": "1.2.0",
"inquirer": "6.0.0", "insert-text-at-cursor": "0.1.1",
"is-root": "2.0.0", "is-root": "2.0.0",
"is-url": "1.2.4", "is-url": "1.2.4",
"jquery": "3.3.1",
"js-yaml": "3.12.0", "js-yaml": "3.12.0",
"jsdom": "11.11.0", "jsdom": "11.12.0",
"koa": "2.5.1", "koa": "2.5.1",
"koa-bodyparser": "4.2.1", "koa-bodyparser": "4.2.1",
"koa-compress": "3.0.0", "koa-compress": "3.0.0",
@ -145,32 +146,30 @@
"koa-send": "5.0.0", "koa-send": "5.0.0",
"koa-slow": "2.1.0", "koa-slow": "2.1.0",
"koa-views": "6.1.4", "koa-views": "6.1.4",
"kue": "0.11.6",
"license-checker": "20.1.0",
"loader-utils": "1.1.0", "loader-utils": "1.1.0",
"mecab-async": "0.1.2", "mecab-async": "0.1.2",
"minio": "6.0.0",
"mkdirp": "0.5.1", "mkdirp": "0.5.1",
"mocha": "5.2.0", "mocha": "5.2.0",
"moji": "0.5.1", "moji": "0.5.1",
"mongodb": "3.1.0", "mongodb": "3.1.1",
"monk": "6.0.6", "monk": "6.0.6",
"ms": "2.1.1", "ms": "2.1.1",
"nan": "2.10.0", "nan": "2.10.0",
"node-sass": "4.9.0", "node-sass": "4.9.2",
"node-sass-json-importer": "3.3.1", "node-sass-json-importer": "3.3.1",
"nopt": "4.0.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"object-assign-deep": "0.4.0", "object-assign-deep": "0.4.0",
"on-build-webpack": "0.1.0", "on-build-webpack": "0.1.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"parse5": "5.0.0", "parse5": "5.0.0",
"portscanner": "2.2.0",
"progress-bar-webpack-plugin": "1.11.0", "progress-bar-webpack-plugin": "1.11.0",
"prominence": "0.2.0",
"promise-sequential": "1.1.1", "promise-sequential": "1.1.1",
"pug": "2.0.3", "pug": "2.0.3",
"punycode": "2.1.1", "punycode": "2.1.1",
"qrcode": "1.2.0", "qrcode": "1.2.2",
"ratelimiter": "3.1.0", "ratelimiter": "3.2.0",
"recaptcha-promise": "0.1.3", "recaptcha-promise": "0.1.3",
"reconnecting-websocket": "3.2.2", "reconnecting-websocket": "3.2.2",
"redis": "2.8.0", "redis": "2.8.0",
@ -181,22 +180,24 @@
"s-age": "1.1.2", "s-age": "1.1.2",
"sass-loader": "7.0.3", "sass-loader": "7.0.3",
"seedrandom": "2.4.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", "single-line-log": "1.1.2",
"speakeasy": "2.0.0", "speakeasy": "2.0.0",
"style-loader": "0.21.0", "style-loader": "0.21.0",
"stylus": "0.54.5", "stylus": "0.54.5",
"stylus-loader": "3.0.2", "stylus-loader": "3.0.2",
"summaly": "2.0.6", "summaly": "2.0.6",
"swagger-jsdoc": "1.9.7", "systeminformation": "3.42.4",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"tcp-port-used": "0.1.2",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"tmp": "0.0.33", "tmp": "0.0.33",
"ts-loader": "4.4.1", "ts-loader": "4.4.1",
"ts-node": "7.0.0", "ts-node": "7.0.0",
"tslint": "5.10.0", "tslint": "5.10.0",
"typescript": "2.9.2", "typescript": "2.9.2",
"typescript-eslint-parser": "16.0.1", "typescript-eslint-parser": "17.0.1",
"uglify-es": "3.3.9", "uglify-es": "3.3.9",
"url-loader": "1.0.1", "url-loader": "1.0.1",
"uuid": "3.3.2", "uuid": "3.3.2",
@ -205,18 +206,19 @@
"vue-cropperjs": "2.2.1", "vue-cropperjs": "2.2.1",
"vue-js-modal": "1.3.16", "vue-js-modal": "1.3.16",
"vue-json-tree-view": "2.1.4", "vue-json-tree-view": "2.1.4",
"vue-loader": "15.2.4", "vue-loader": "15.2.6",
"vue-router": "3.0.1", "vue-router": "3.0.1",
"vue-style-loader": "4.1.1",
"vue-template-compiler": "2.5.16", "vue-template-compiler": "2.5.16",
"vuedraggable": "2.16.0", "vuedraggable": "2.16.0",
"vuex": "3.0.1", "vuex": "3.0.1",
"vuex-persistedstate": "^2.5.4", "vuex-persistedstate": "2.5.4",
"web-push": "3.3.2", "web-push": "3.3.2",
"webfinger.js": "2.6.6", "webfinger.js": "2.6.6",
"webpack": "4.14.0", "webpack": "4.16.3",
"webpack-cli": "3.0.8", "webpack-cli": "3.1.0",
"websocket": "1.0.26", "websocket": "1.0.26",
"ws": "5.2.1", "ws": "6.0.0",
"xev": "2.0.1" "xev": "2.0.1"
}, },
"greenkeeper": { "greenkeeper": {

View file

@ -1,5 +0,0 @@
import { IUser } from '../models/user';
export default (user: IUser) => {
return user.host === null ? user.username : `${user.username}@${user.host}`;
};

View file

@ -2,7 +2,7 @@
<div class="form"> <div class="form">
<header> <header>
<h1><i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか</h1> <h1><i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか</h1>
<img :src="`${app.iconUrl}?thumbnail&size=64`"/> <img :src="app.iconUrl"/>
</header> </header>
<div class="app"> <div class="app">
<section> <section>

View file

@ -1,6 +1,6 @@
import getNoteSummary from '../../../../renderers/get-note-summary'; import getNoteSummary from '../../../../misc/get-note-summary';
import getReactionEmoji from '../../../../renderers/get-reaction-emoji'; import getReactionEmoji from '../../../../misc/get-reaction-emoji';
import getUserName from '../../../../renderers/get-user-name'; import getUserName from '../../../../misc/get-user-name';
type Notification = { type Notification = {
title: string; title: string;
@ -17,21 +17,21 @@ export default function(type, data): Notification {
return { return {
title: 'ファイルがアップロードされました', title: 'ファイルがアップロードされました',
body: data.name, body: data.name,
icon: data.url + '?thumbnail&size=64' icon: data.url
}; };
case 'unread_messaging_message': case 'unread_messaging_message':
return { return {
title: `${getUserName(data.user)}さんからメッセージ:`, title: `${getUserName(data.user)}さんからメッセージ:`,
body: data.text, // TODO: getMessagingMessageSummary(data), body: data.text, // TODO: getMessagingMessageSummary(data),
icon: data.user.avatarUrl + '?thumbnail&size=64' icon: data.user.avatarUrl
}; };
case 'reversi_invited': case 'reversi_invited':
return { return {
title: '対局への招待があります', title: '対局への招待があります',
body: `${getUserName(data.parent)}さんから`, body: `${getUserName(data.parent)}さんから`,
icon: data.parent.avatarUrl + '?thumbnail&size=64' icon: data.parent.avatarUrl
}; };
case 'notification': case 'notification':
@ -40,28 +40,28 @@ export default function(type, data): Notification {
return { return {
title: `${getUserName(data.user)}さんから:`, title: `${getUserName(data.user)}さんから:`,
body: getNoteSummary(data), body: getNoteSummary(data),
icon: data.user.avatarUrl + '?thumbnail&size=64' icon: data.user.avatarUrl
}; };
case 'reply': case 'reply':
return { return {
title: `${getUserName(data.user)}さんから返信:`, title: `${getUserName(data.user)}さんから返信:`,
body: getNoteSummary(data), body: getNoteSummary(data),
icon: data.user.avatarUrl + '?thumbnail&size=64' icon: data.user.avatarUrl
}; };
case 'quote': case 'quote':
return { return {
title: `${getUserName(data.user)}さんが引用:`, title: `${getUserName(data.user)}さんが引用:`,
body: getNoteSummary(data), body: getNoteSummary(data),
icon: data.user.avatarUrl + '?thumbnail&size=64' icon: data.user.avatarUrl
}; };
case 'reaction': case 'reaction':
return { return {
title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`, title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`,
body: getNoteSummary(data.note), body: getNoteSummary(data.note),
icon: data.user.avatarUrl + '?thumbnail&size=64' icon: data.user.avatarUrl
}; };
default: default:

View file

@ -1,9 +1,9 @@
import Stream from './stream'; import Stream from '../../stream';
import MiOS from '../../../mios'; import MiOS from '../../../../../mios';
export class ReversiGameStream extends Stream { export class ReversiGameStream extends Stream {
constructor(os: MiOS, me, game) { constructor(os: MiOS, me, game) {
super(os, 'reversi-game', { super(os, 'games/reversi-game', {
i: me ? me.token : null, i: me ? me.token : null,
game: game.id game: game.id
}); });

View file

@ -1,10 +1,10 @@
import StreamManager from './stream-manager'; import StreamManager from '../../stream-manager';
import Stream from './stream'; import Stream from '../../stream';
import MiOS from '../../../mios'; import MiOS from '../../../../../mios';
export class ReversiStream extends Stream { export class ReversiStream extends Stream {
constructor(os: MiOS, me) { constructor(os: MiOS, me) {
super(os, 'reversi', { super(os, 'games/reversi', {
i: me.token i: me.token
}); });
} }

View 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;
}
}

View file

@ -39,13 +39,17 @@ export default Vue.extend({
dark: { dark: {
type: Boolean, type: Boolean,
default: false default: false
},
smooth: {
type: Boolean,
default: false
} }
}, },
data() { data() {
return { return {
now: new Date(), now: new Date(),
clock: null, enabled: true,
graduationsPadding: 0.5, graduationsPadding: 0.5,
handsPadding: 1, handsPadding: 1,
@ -74,6 +78,9 @@ export default Vue.extend({
return themeColor; return themeColor;
}, },
ms(): number {
return this.now.getMilliseconds() * this.smooth;
}
s(): number { s(): number {
return this.now.getSeconds(); return this.now.getSeconds();
}, },
@ -85,13 +92,13 @@ export default Vue.extend({
}, },
hAngle(): number { 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 { 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 { sAngle(): number {
return Math.PI * this.s / 30; return Math.PI * (this.s + this.ms / 1000) / 30;
}, },
graduations(): any { graduations(): any {
@ -106,11 +113,17 @@ export default Vue.extend({
}, },
mounted() { mounted() {
this.clock = setInterval(this.tick, 1000); const update = () => {
if (this.enabled) {
this.tick();
requestAnimationFrame(update);
}
};
update();
}, },
beforeDestroy() { beforeDestroy() {
clearInterval(this.clock); this.enabled = false;
}, },
methods: { methods: {

View file

@ -2,11 +2,16 @@
<div class="mk-autocomplete" @contextmenu.prevent="() => {}"> <div class="mk-autocomplete" @contextmenu.prevent="() => {}">
<ol class="users" ref="suggests" v-if="users.length > 0"> <ol class="users" ref="suggests" v-if="users.length > 0">
<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1"> <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="name">{{ user | userName }}</span>
<span class="username">@{{ user | acct }}</span> <span class="username">@{{ user | acct }}</span>
</li> </li>
</ol> </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"> <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"> <li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
<span class="emoji">{{ emoji.emoji }}</span> <span class="emoji">{{ emoji.emoji }}</span>
@ -48,33 +53,33 @@ emjdb.sort((a, b) => a.name.length - b.name.length);
export default Vue.extend({ export default Vue.extend({
props: ['type', 'q', 'textarea', 'complete', 'close', 'x', 'y'], props: ['type', 'q', 'textarea', 'complete', 'close', 'x', 'y'],
data() { data() {
return { return {
fetching: true, fetching: true,
users: [], users: [],
hashtags: [],
emojis: [], emojis: [],
select: -1, select: -1,
emojilib emojilib
} }
}, },
computed: { computed: {
items(): HTMLCollection { items(): HTMLCollection {
return (this.$refs.suggests as Element).children; return (this.$refs.suggests as Element).children;
} }
}, },
updated() { updated() {
//#region 調 //#region 調
const margin = 32; if (this.x + this.$el.offsetWidth > window.innerWidth) {
this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px';
if (this.x + this.$el.offsetWidth > window.innerWidth - margin) {
this.$el.style.left = (this.x - this.$el.offsetWidth) + 'px';
this.$el.style.marginLeft = '-16px';
} else { } else {
this.$el.style.left = this.x + 'px'; 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.top = (this.y - this.$el.offsetHeight) + 'px';
this.$el.style.marginTop = '0'; this.$el.style.marginTop = '0';
} else { } else {
@ -83,6 +88,7 @@ export default Vue.extend({
} }
//#endregion //#endregion
}, },
mounted() { mounted() {
this.textarea.addEventListener('keydown', this.onKeydown); this.textarea.addEventListener('keydown', this.onKeydown);
@ -100,6 +106,7 @@ export default Vue.extend({
}); });
}); });
}, },
beforeDestroy() { beforeDestroy() {
this.textarea.removeEventListener('keydown', this.onKeydown); this.textarea.removeEventListener('keydown', this.onKeydown);
@ -107,6 +114,7 @@ export default Vue.extend({
el.removeEventListener('mousedown', this.onMousedown); el.removeEventListener('mousedown', this.onMousedown);
}); });
}, },
methods: { methods: {
exec() { exec() {
this.select = -1; this.select = -1;
@ -117,7 +125,8 @@ export default Vue.extend({
} }
if (this.type == 'user') { if (this.type == 'user') {
const cache = sessionStorage.getItem(this.q); const cacheKey = 'autocomplete:user:' + this.q;
const cache = sessionStorage.getItem(cacheKey);
if (cache) { if (cache) {
const users = JSON.parse(cache); const users = JSON.parse(cache);
this.users = users; this.users = users;
@ -131,9 +140,33 @@ export default Vue.extend({
this.fetching = false; 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') { } else if (this.type == 'emoji') {
const matched = []; const matched = [];
emjdb.some(x => { emjdb.some(x => {
@ -228,12 +261,13 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
@import '~const.styl' @import '~const.styl'
.mk-autocomplete root(isDark)
position fixed position fixed
z-index 65535 z-index 65535
max-width 100%
margin-top calc(1em + 8px) margin-top calc(1em + 8px)
overflow hidden overflow hidden
background #fff background isDark ? #313543 : #fff
border solid 1px rgba(#000, 0.1) border solid 1px rgba(#000, 0.1)
border-radius 4px border-radius 4px
transition top 0.1s ease, left 0.1s ease transition top 0.1s ease, left 0.1s ease
@ -248,7 +282,8 @@ export default Vue.extend({
list-style none list-style none
> li > li
display block display flex
align-items center
padding 4px 12px padding 4px 12px
white-space nowrap white-space nowrap
overflow hidden overflow hidden
@ -259,7 +294,13 @@ export default Vue.extend({
&, * &, *
user-select none user-select none
*
overflow hidden
text-overflow ellipsis
&:hover &:hover
background isDark ? rgba(#fff, 0.1) : rgba(#000, 0.1)
&[data-selected='true'] &[data-selected='true']
background $theme-color background $theme-color
@ -275,7 +316,6 @@ export default Vue.extend({
> .users > li > .users > li
.avatar .avatar
vertical-align middle
min-width 28px min-width 28px
min-height 28px min-height 28px
max-width 28px max-width 28px
@ -285,10 +325,15 @@ export default Vue.extend({
.name .name
margin 0 8px 0 0 margin 0 8px 0 0
color rgba(#000, 0.8) color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8)
.username .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 > .emojis > li
@ -298,10 +343,15 @@ export default Vue.extend({
width 24px width 24px
.name .name
color rgba(#000, 0.8) color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8)
.alias .alias
margin 0 0 0 8px 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> </style>

View file

@ -31,7 +31,7 @@ export default Vue.extend({
: this.user.avatarColor && this.user.avatarColor.length == 3 : this.user.avatarColor && this.user.avatarColor.length == 3
? `rgb(${ this.user.avatarColor.join(',') })` ? `rgb(${ this.user.avatarColor.join(',') })`
: null, : 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 borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
}; };
} }

View file

@ -8,7 +8,7 @@
<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">%i18n:common.reversi.opponent-turn%<mk-ellipsis/></p> <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="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"> <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> <template v-else>%i18n:common.reversi.drawn%</template>
</p> </p>
</div> </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 }" :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)" @click="set(i)"
:title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"> :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 === true" :src="blackUser.avatarUrl" alt="">
<img v-if="stone === false" :src="`${whiteUser.avatarUrl}?thumbnail&size=128`" alt=""> <img v-if="stone === false" :src="whiteUser.avatarUrl" alt="">
</div> </div>
</div> </div>
<div class="labels-y" v-if="this.$store.state.settings.reversiBoardLabels"> <div class="labels-y" v-if="this.$store.state.settings.reversiBoardLabels">
@ -58,8 +58,8 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import * as CRC32 from 'crc-32'; import * as CRC32 from 'crc-32';
import Reversi, { Color } from '../../../../../reversi/core'; import Reversi, { Color } from '../../../../../../../games/reversi/core';
import { url } from '../../../config'; import { url } from '../../../../../config';
export default Vue.extend({ export default Vue.extend({
props: ['initGame', 'connection'], props: ['initGame', 'connection'],
@ -105,13 +105,14 @@ export default Vue.extend({
} }
}, },
isMyTurn(): boolean { 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; return this.turnUser.id == this.$store.state.i.id;
}, },
cellsStyle(): any { cellsStyle(): any {
return { return {
'grid-template-rows': `repeat(${ this.game.settings.map.length }, 1fr)`, 'grid-template-rows': `repeat(${this.game.settings.map.length}, 1fr)`,
'grid-template-columns': `repeat(${ this.game.settings.map[0].length }, 1fr)` 'grid-template-columns': `repeat(${this.game.settings.map[0].length}, 1fr)`
}; };
} }
}, },

View file

@ -9,7 +9,7 @@
import Vue from 'vue'; import Vue from 'vue';
import XGame from './reversi.game.vue'; import XGame from './reversi.game.vue';
import XRoom from './reversi.room.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({ export default Vue.extend({
components: { components: {

View file

@ -94,7 +94,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import * as maps from '../../../../../reversi/maps'; import * as maps from '../../../../../../../games/reversi/maps';
export default Vue.extend({ export default Vue.extend({
props: ['game', 'connection'], props: ['game', 'connection'],
@ -112,7 +112,7 @@ export default Vue.extend({
computed: { computed: {
mapCategories(): string[] { 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); return categories.filter((item, pos) => categories.indexOf(item) == pos);
}, },
isAccepted(): boolean { isAccepted(): boolean {
@ -179,8 +179,8 @@ export default Vue.extend({
if (this.game.settings.map == null) { if (this.game.settings.map == null) {
this.mapName = null; this.mapName = null;
} else { } else {
const foundMap = Object.entries(maps).find(x => x[1].data.join('') == this.game.settings.map.join('')); const found = Object.values(maps).find(x => x.data.join('') == this.game.settings.map.join(''));
this.mapName = foundMap ? foundMap[1].name : '-Custom-'; this.mapName = found ? found.name : '-Custom-';
} }
}, },
@ -206,7 +206,7 @@ export default Vue.extend({
if (v == null) { if (v == null) {
this.game.settings.map = null; this.game.settings.map = null;
} else { } 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.$forceUpdate();
this.updateSettings(); this.updateSettings();

View file

@ -67,7 +67,9 @@ export default Vue.extend({
components: { components: {
XGameroom XGameroom
}, },
props: ['initGame'], props: ['initGame'],
data() { data() {
return { return {
game: null, game: null,
@ -82,35 +84,34 @@ export default Vue.extend({
pingClock: null pingClock: null
}; };
}, },
watch: { watch: {
game(g) { game(g) {
this.$emit('gamed', g); this.$emit('gamed', g);
} }
}, },
created() { created() {
if (this.initGame) { if (this.initGame) {
this.game = this.initGame; this.game = this.initGame;
} }
}, },
mounted() { mounted() {
if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.streams.reversiStream.getConnection(); this.connection = (this as any).os.streams.reversiStream.getConnection();
this.connectionId = (this as any).os.streams.reversiStream.use(); this.connectionId = (this as any).os.streams.reversiStream.use();
this.connection.on('matched', this.onMatched); this.connection.on('matched', this.onMatched);
this.connection.on('invited', this.onInvited); this.connection.on('invited', this.onInvited);
(this as any).api('reversi/games', { (this as any).api('games/reversi/games', {
my: true my: true
}).then(games => { }).then(games => {
this.myGames = games; this.myGames = games;
}); });
(this as any).api('reversi/games').then(games => { (this as any).api('games/reversi/invitations').then(invitations => {
this.games = games;
this.gamesFetching = false;
});
(this as any).api('reversi/invitations').then(invitations => {
this.invitations = this.invitations.concat(invitations); this.invitations = this.invitations.concat(invitations);
}); });
@ -122,23 +123,34 @@ export default Vue.extend({
}); });
} }
}, 3000); }, 3000);
}
(this as any).api('games/reversi/games').then(games => {
this.games = games;
this.gamesFetching = false;
});
}, },
beforeDestroy() { beforeDestroy() {
if (this.connection) {
this.connection.off('matched', this.onMatched); this.connection.off('matched', this.onMatched);
this.connection.off('invited', this.onInvited); this.connection.off('invited', this.onInvited);
(this as any).os.streams.reversiStream.dispose(this.connectionId); (this as any).os.streams.reversiStream.dispose(this.connectionId);
clearInterval(this.pingClock); clearInterval(this.pingClock);
}
}, },
methods: { methods: {
go(game) { go(game) {
(this as any).api('reversi/games/show', { (this as any).api('games/reversi/games/show', {
gameId: game.id gameId: game.id
}).then(game => { }).then(game => {
this.matching = null; this.matching = null;
this.game = game; this.game = game;
}); });
}, },
match() { match() {
(this as any).apis.input({ (this as any).apis.input({
title: 'ユーザー名を入力してください' title: 'ユーザー名を入力してください'
@ -146,7 +158,7 @@ export default Vue.extend({
(this as any).api('users/show', { (this as any).api('users/show', {
username username
}).then(user => { }).then(user => {
(this as any).api('reversi/match', { (this as any).api('games/reversi/match', {
userId: user.id userId: user.id
}).then(res => { }).then(res => {
if (res == null) { if (res == null) {
@ -158,12 +170,14 @@ export default Vue.extend({
}); });
}); });
}, },
cancel() { cancel() {
this.matching = null; this.matching = null;
(this as any).api('reversi/match/cancel'); (this as any).api('games/reversi/match/cancel');
}, },
accept(invitation) { accept(invitation) {
(this as any).api('reversi/match', { (this as any).api('games/reversi/match', {
userId: invitation.parent.id userId: invitation.parent.id
}).then(game => { }).then(game => {
if (game) { if (game) {
@ -172,10 +186,12 @@ export default Vue.extend({
} }
}); });
}, },
onMatched(game) { onMatched(game) {
this.matching = null; this.matching = null;
this.game = game; this.game = game;
}, },
onInvited(invite) { onInvited(invite) {
this.invitations.unshift(invite); this.invitations.unshift(invite);
} }

View file

@ -27,7 +27,7 @@ import urlPreview from './url-preview.vue';
import twitterSetting from './twitter-setting.vue'; import twitterSetting from './twitter-setting.vue';
import fileTypeIcon from './file-type-icon.vue'; import fileTypeIcon from './file-type-icon.vue';
import Switch from './switch.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 welcomeTimeline from './welcome-timeline.vue';
import uiInput from './ui/input.vue'; import uiInput from './ui/input.vue';
import uiButton from './ui/button.vue'; import uiButton from './ui/button.vue';

View file

@ -46,33 +46,45 @@ export default Vue.extend({
display grid display grid
grid-gap 4px grid-gap 4px
> *
overflow hidden
border-radius 4px
&[data-count="1"] &[data-count="1"]
grid-template-rows 1fr grid-template-rows 1fr
&[data-count="2"] &[data-count="2"]
grid-template-columns 1fr 1fr grid-template-columns 1fr 1fr
grid-template-rows 1fr grid-template-rows 1fr
&[data-count="3"] &[data-count="3"]
grid-template-columns 1fr 0.5fr grid-template-columns 1fr 0.5fr
grid-template-rows 1fr 1fr grid-template-rows 1fr 1fr
:nth-child(1)
> *:nth-child(1)
grid-row 1 / 3 grid-row 1 / 3
:nth-child(3)
> *:nth-child(3)
grid-column 2 / 3 grid-column 2 / 3
grid-row 2 / 3 grid-row 2 / 3
&[data-count="4"] &[data-count="4"]
grid-template-columns 1fr 1fr grid-template-columns 1fr 1fr
grid-template-rows 1fr 1fr grid-template-rows 1fr 1fr
:nth-child(1) > *:nth-child(1)
grid-column 1 / 2 grid-column 1 / 2
grid-row 1 / 2 grid-row 1 / 2
:nth-child(2)
> *:nth-child(2)
grid-column 2 / 3 grid-column 2 / 3
grid-row 1 / 2 grid-row 1 / 2
:nth-child(3)
> *:nth-child(3)
grid-column 1 / 2 grid-column 1 / 2
grid-row 2 / 3 grid-row 2 / 3
:nth-child(4)
> *:nth-child(4)
grid-column 2 / 3 grid-column 2 / 3
grid-row 2 / 3 grid-row 2 / 3

View file

@ -119,7 +119,7 @@ export default Vue.extend({
}, },
onKeypress(e) { onKeypress(e) {
if ((e.which == 10 || e.which == 13) && e.ctrlKey) { if ((e.which == 10 || e.which == 13) && e.ctrlKey && this.canSend) {
this.send(); this.send();
} }
}, },

View file

@ -3,10 +3,9 @@
<mk-avatar class="avatar" :user="message.user" target="_blank"/> <mk-avatar class="avatar" :user="message.user" target="_blank"/>
<div class="content"> <div class="content">
<div class="balloon" :data-no-text="message.text == null"> <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"/> <img src="/assets/desktop/messaging/delete.png" alt="Delete"/>
</button> </button> -->
<div class="content" v-if="!message.isDeleted"> <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"/> <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"> <div class="file" v-if="message.file">
@ -23,6 +22,7 @@
<div></div> <div></div>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/> <mk-url-preview v-for="url in urls" :url="url" :key="url"/>
<footer> <footer>
<span class="read" v-if="isMe && message.isRead">%i18n:@is-read%</span>
<mk-time :time="message.createdAt"/> <mk-time :time="message.createdAt"/>
<template v-if="message.is_edited">%fa:pencil-alt%</template> <template v-if="message.is_edited">%fa:pencil-alt%</template>
</footer> </footer>
@ -120,17 +120,6 @@ root(isDark)
height 16px height 16px
cursor pointer 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 > .content
> .is-deleted > .is-deleted
@ -258,6 +247,12 @@ root(isDark)
> footer > footer
text-align right 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] &[data-is-deleted]
> .baloon > .baloon
opacity 0.5 opacity 0.5

View file

@ -51,7 +51,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import getAcct from '../../../../../acct/render'; import getAcct from '../../../../../misc/acct/render';
export default Vue.extend({ export default Vue.extend({
props: { props: {

View file

@ -1,7 +1,7 @@
import Vue from 'vue'; import Vue from 'vue';
import * as emojilib from 'emojilib'; import * as emojilib from 'emojilib';
import parse from '../../../../../mfm/parse'; import parse from '../../../../../mfm/parse';
import getAcct from '../../../../../acct/render'; import getAcct from '../../../../../misc/acct/render';
import { url } from '../../../config'; import { url } from '../../../config';
import MkUrl from './url.vue'; import MkUrl from './url.vue';
import MkGoogle from './google.vue'; import MkGoogle from './google.vue';
@ -92,7 +92,7 @@ export default Vue.component('misskey-flavored-markdown', {
case 'hashtag': case 'hashtag':
return createElement('a', { return createElement('a', {
attrs: { attrs: {
href: `${url}/tags/${token.hashtag}`, href: `${url}/tags/${encodeURIComponent(token.hashtag)}`,
target: '_blank' target: '_blank'
} }
}, token.content); }, token.content);

View file

@ -2,9 +2,9 @@
<span class="mk-nav"> <span class="mk-nav">
<a :href="aboutUrl">%i18n:@about%</a> <a :href="aboutUrl">%i18n:@about%</a>
<i></i> <i></i>
<a href="https://github.com/syuilo/misskey">%i18n:@repository%</a> <a :href="repositoryUrl">%i18n:@repository%</a>
<i></i> <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> <i></i>
<a :href="devUrl">%i18n:@develop%</a> <a :href="devUrl">%i18n:@develop%</a>
<i></i> <i></i>
@ -14,7 +14,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; 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({ export default Vue.extend({
data() { data() {
@ -22,7 +22,9 @@ export default Vue.extend({
aboutUrl: `${docsUrl}/${lang}/about`, aboutUrl: `${docsUrl}/${lang}/about`,
statsUrl, statsUrl,
statusUrl, statusUrl,
devUrl devUrl,
repositoryUrl: repositoryUrl || `https://github.com/syuilo/misskey`,
feedbackUrl: feedbackUrl || `https://github.com/syuilo/misskey/issues/new`
} }
} }
}); });

View file

@ -2,6 +2,7 @@
<header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu"> <header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu">
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/> <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> <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-admin" v-if="note.user.isAdmin">admin</span>
<span class="is-bot" v-if="note.user.isBot">bot</span> <span class="is-bot" v-if="note.user.isBot">bot</span>
<span class="is-cat" v-if="note.user.isCat">cat</span> <span class="is-cat" v-if="note.user.isCat">cat</span>
@ -69,6 +70,10 @@ root(isDark)
&:hover &:hover
text-decoration underline text-decoration underline
> .is-verified
margin-right 8px
color #4dabf7
> .is-admin > .is-admin
> .is-bot > .is-bot
> .is-cat > .is-cat

View file

@ -183,7 +183,7 @@ root(isDark)
border-right solid $balloon-size transparent border-right solid $balloon-size transparent
border-bottom solid $balloon-size $bgcolor border-bottom solid $balloon-size $bgcolor
&.compact &.big
> div > div
width 280px width 280px

View file

@ -29,11 +29,7 @@
<p slot="text" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@password-not-matched%</p> <p slot="text" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@password-not-matched%</p>
</div> </div>
</ui-input> </ui-input>
<div class="g-recaptcha" :data-sitekey="recaptchaSitekey" style="margin: 16px 0;"></div> <div v-if="recaptchaSitekey != null" 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>
<ui-button type="submit">%i18n:@create%</ui-button> <ui-button type="submit">%i18n:@create%</ui-button>
</form> </form>
</template> </template>
@ -41,7 +37,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
const getPasswordStrength = require('syuilo-password-strength'); const getPasswordStrength = require('syuilo-password-strength');
import { host, url, docsUrl, lang, recaptchaSitekey } from '../../../config'; import { host, url, recaptchaSitekey } from '../../../config';
export default Vue.extend({ export default Vue.extend({
data() { data() {
@ -51,7 +47,6 @@ export default Vue.extend({
password: '', password: '',
retypedPassword: '', retypedPassword: '',
url, url,
touUrl: `${docsUrl}/${lang}/tou`,
recaptchaSitekey, recaptchaSitekey,
usernameState: null, usernameState: null,
passwordStrength: '', passwordStrength: '',
@ -115,7 +110,7 @@ export default Vue.extend({
(this as any).api('signup', { (this as any).api('signup', {
username: this.username, username: this.username,
password: this.password, password: this.password,
'g-recaptcha-response': (window as any).grecaptcha.getResponse() 'g-recaptcha-response': recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null
}).then(() => { }).then(() => {
(this as any).api('signin', { (this as any).api('signin', {
username: this.username, username: this.username,
@ -126,16 +121,20 @@ export default Vue.extend({
}).catch(() => { }).catch(() => {
alert('%i18n:@some-error%'); alert('%i18n:@some-error%');
if (recaptchaSitekey != null) {
(window as any).grecaptcha.reset(); (window as any).grecaptcha.reset();
}
}); });
} }
}, },
mounted() { mounted() {
if (recaptchaSitekey != null) {
const head = document.getElementsByTagName('head')[0]; const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script'); const script = document.createElement('script');
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js'); script.setAttribute('src', 'https://www.google.com/recaptcha/api.js');
head.appendChild(script); head.appendChild(script);
} }
}
}); });
</script> </script>
@ -144,22 +143,4 @@ export default Vue.extend({
.mk-signup .mk-signup
min-width 302px min-width 302px
.agree-tou
padding 4px
border-radius 4px
&:hover
background #f4f4f4
&:active
background #eee
&, *
cursor pointer
p
display inline
color #555
</style> </style>

View file

@ -2,6 +2,11 @@
<iframe v-if="youtubeId" type="text/html" height="250" <iframe v-if="youtubeId" type="text/html" height="250"
:src="`https://www.youtube.com/embed/${youtubeId}?origin=${misskeyUrl}`" :src="`https://www.youtube.com/embed/${youtubeId}?origin=${misskeyUrl}`"
frameborder="0"/> 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"> <div v-else class="mk-url-preview">
<a :href="url" target="_blank" :title="url" v-if="!fetching"> <a :href="url" target="_blank" :title="url" v-if="!fetching">
<div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div> <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'; import { url as misskeyUrl } from '../../../config';
export default Vue.extend({ export default Vue.extend({
props: ['url'], props: {
url: {
type: String,
require: true
},
detail: {
type: Boolean,
required: false,
default: false
}
},
data() { data() {
return { return {
fetching: true, fetching: true,
@ -34,6 +49,7 @@ export default Vue.extend({
icon: null, icon: null,
sitename: null, sitename: null,
youtubeId: null, youtubeId: null,
tweetUrl: null,
misskeyUrl misskeyUrl
}; };
}, },
@ -44,6 +60,25 @@ export default Vue.extend({
this.youtubeId = url.searchParams.get('v'); this.youtubeId = url.searchParams.get('v');
} else if (url.hostname == 'youtu.be') { } else if (url.hostname == 'youtu.be') {
this.youtubeId = url.pathname; 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 { } else {
fetch('/url?url=' + encodeURIComponent(this.url)).then(res => { fetch('/url?url=' + encodeURIComponent(this.url)).then(res => {
res.json().then(info => { res.json().then(info => {

View file

@ -1,5 +1,6 @@
import * as getCaretCoordinates from 'textarea-caret'; import * as getCaretCoordinates from 'textarea-caret';
import MkAutocomplete from '../components/autocomplete.vue'; import MkAutocomplete from '../components/autocomplete.vue';
import renderAcct from '../../../../../misc/acct/render';
export default { export default {
bind(el, binding, vn) { bind(el, binding, vn) {
@ -67,15 +68,30 @@ class Autocomplete {
* *
*/ */
private onInput() { private onInput() {
const caret = this.textarea.selectionStart; const caretPos = this.textarea.selectionStart;
const text = this.text.substr(0, caret); const text = this.text.substr(0, caretPos).split('\n').pop();
const mentionIndex = text.lastIndexOf('@'); const mentionIndex = text.lastIndexOf('@');
const hashtagIndex = text.lastIndexOf('#');
const emojiIndex = 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; let opened = false;
if (mentionIndex != -1 && mentionIndex > emojiIndex) { if (isMention) {
const username = text.substr(mentionIndex + 1); const username = text.substr(mentionIndex + 1);
if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) { if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) {
this.open('user', username); 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); const emoji = text.substr(emojiIndex + 1);
if (emoji != '' && emoji.match(/^[\+\-a-z0-9_]+$/)) { if (emoji != '' && emoji.match(/^[\+\-a-z0-9_]+$/)) {
this.open('emoji', emoji); this.open('emoji', emoji);
@ -164,13 +188,31 @@ class Autocomplete {
const trimmedBefore = before.substring(0, before.lastIndexOf('@')); const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
const after = source.substr(caret); const after = source.substr(caret);
const acct = renderAcct(value);
// 挿入 // 挿入
this.text = trimmedBefore + '@' + value.username + ' ' + after; this.text = trimmedBefore + '@' + acct + ' ' + after;
// キャレットを戻す // キャレットを戻す
this.vm.$nextTick(() => { this.vm.$nextTick(() => {
this.textarea.focus(); 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); this.textarea.setSelectionRange(pos, pos);
}); });
} else if (type == 'emoji') { } else if (type == 'emoji') {

View file

@ -1,6 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import getAcct from '../../../../../acct/render'; import getAcct from '../../../../../misc/acct/render';
import getUserName from '../../../../../renderers/get-user-name'; import getUserName from '../../../../../misc/get-user-name';
Vue.filter('acct', user => { Vue.filter('acct', user => {
return getAcct(user); return getAcct(user);

View file

@ -31,8 +31,8 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import parseAcct from '../../../../../acct/parse'; import parseAcct from '../../../../../misc/acct/parse';
import getUserName from '../../../../../renderers/get-user-name'; import getUserName from '../../../../../misc/get-user-name';
import Progress from '../../../common/scripts/loading'; import Progress from '../../../common/scripts/loading';
export default Vue.extend({ export default Vue.extend({

View file

@ -1,8 +1,8 @@
<template> <template>
<div class="mkw-analog-clock"> <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"> <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> </div>
</mk-widget-container> </mk-widget-container>
</div> </div>
@ -13,12 +13,13 @@ import define from '../../../common/define-widget';
export default define({ export default define({
name: 'analog-clock', name: 'analog-clock',
props: () => ({ props: () => ({
naked: false design: -1
}) })
}).extend({ }).extend({
methods: { methods: {
func() { func() {
this.props.naked = !this.props.naked; if (++this.props.design > 2)
this.props.design = -1;
this.save(); this.save();
} }
} }

View file

@ -175,6 +175,7 @@ root(isDark)
> .val > .val
height 4px height 4px
background $theme-color background $theme-color
transition width .3s cubic-bezier(0.23, 1, 0.32, 1)
&:nth-child(1) &:nth-child(1)
> .meter > .val > .meter > .val

View file

@ -11,7 +11,7 @@
<div> <div>
<div v-for="stat in stats" :key="stat.tag"> <div v-for="stat in stats" :key="stat.tag">
<div class="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> <p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p>
</div> </div>
<x-chart class="chart" :src="stat.chart"/> <x-chart class="chart" :src="stat.chart"/>

View file

@ -5,7 +5,7 @@
<p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> <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 :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> </div>
<p :class="$style.empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p> <p :class="$style.empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p>
</mk-widget-container> </mk-widget-container>

View file

@ -102,7 +102,6 @@ export default Vue.extend({
}, },
methods: { methods: {
onStats(stats) { onStats(stats) {
stats.mem.used = stats.mem.total - stats.mem.free;
this.stats.push(stats); this.stats.push(stats);
if (this.stats.length > 50) this.stats.shift(); 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.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.memPolylinePoints = memPolylinePoints.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.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.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0]; this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0];
this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1]; this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1];

View file

@ -35,7 +35,7 @@ export default Vue.extend({
}, },
methods: { methods: {
onStats(stats) { 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.usage = stats.mem.used / stats.mem.total;
this.total = stats.mem.total; this.total = stats.mem.total;
this.used = stats.mem.used; this.used = stats.mem.used;

View file

@ -2,10 +2,10 @@
<div class="mkw-slideshow" :data-mobile="platform == 'mobile'"> <div class="mkw-slideshow" :data-mobile="platform == 'mobile'">
<div @click="choose"> <div @click="choose">
<p v-if="props.folder === undefined"> <p v-if="props.folder === undefined">
<template v-if="isCustomizeMode">フォルダを指定するにはカスタマイズモードを終了してください</template> <template v-if="isCustomizeMode">%i18n:@folder-customize-mode%</template>
<template v-else>クリックしてフォルダを指定してください</template> <template v-else>%i18n:@folder%</template>
</p> </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="slideA" class="slide a"></div>
<div ref="slideB" class="slide b"></div> <div ref="slideB" class="slide b"></div>
</div> </div>
@ -72,7 +72,7 @@ export default define({
if (this.images.length == 0) return; if (this.images.length == 0) return;
const index = Math.floor(Math.random() * this.images.length); 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; (this.$refs.slideB as any).style.backgroundImage = img;

View file

@ -9,6 +9,8 @@ declare const _DOCS_URL_: string;
declare const _STATS_URL_: string; declare const _STATS_URL_: string;
declare const _STATUS_URL_: string; declare const _STATUS_URL_: string;
declare const _DEV_URL_: string; declare const _DEV_URL_: string;
declare const _REPOSITORY_URL_: string;
declare const _FEEDBACK_URL_: string;
declare const _LANG_: string; declare const _LANG_: string;
declare const _LANGS_: string; declare const _LANGS_: string;
declare const _RECAPTCHA_SITEKEY_: string; declare const _RECAPTCHA_SITEKEY_: string;
@ -32,6 +34,8 @@ export const docsUrl = _DOCS_URL_;
export const statsUrl = _STATS_URL_; export const statsUrl = _STATS_URL_;
export const statusUrl = _STATUS_URL_; export const statusUrl = _STATUS_URL_;
export const devUrl = _DEV_URL_; export const devUrl = _DEV_URL_;
export const repositoryUrl = _REPOSITORY_URL_;
export const feedbackUrl = _FEEDBACK_URL_;
export const lang = _LANG_; export const lang = _LANG_;
export const langs = _LANGS_; export const langs = _LANGS_;
export const recaptchaSitekey = _RECAPTCHA_SITEKEY_; export const recaptchaSitekey = _RECAPTCHA_SITEKEY_;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 KiB

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 424 B

View file

@ -35,10 +35,7 @@ import Vue from 'vue';
const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
function isLeapYear(year) { function isLeapYear(year) {
return (year % 400 == 0) ? true : return !(year & (year % 25 ? 3 : 15));
(year % 100 == 0) ? false :
(year % 4 == 0) ? true :
false;
} }
export default Vue.extend({ export default Vue.extend({

View file

@ -28,7 +28,7 @@ export default Vue.extend({
default: false default: false
}, },
title: { title: {
default: '%fa:R file%%i18n:@choose-prompt%s' default: '%fa:R file%%i18n:@choose-prompt%'
} }
}, },
data() { data() {

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="root file" <div class="gvfdktuvdgwhmztnuekzkswkjygptfcv"
:data-is-selected="isSelected" :data-is-selected="isSelected"
:data-is-contextmenu-showing="isContextmenuShowing" :data-is-contextmenu-showing="isContextmenuShowing"
@click="onClick" @click="onClick"
@ -16,7 +16,7 @@
<p>%i18n:@banner%</p> <p>%i18n:@banner%</p>
</div> </div>
<div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`"> <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> </div>
<p class="name"> <p class="name">
<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> <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%', icon: '%fa:i-cursor%',
action: this.rename 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', type: 'item',
text: '%i18n:@contextmenu.copy-url%', text: '%i18n:@contextmenu.copy-url%',
icon: '%fa:link%', 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() { copyUrl() {
copyToClipboard(this.file.url); copyToClipboard(this.file.url);
(this as any).apis.dialog({ (this as any).apis.dialog({
@ -312,10 +324,10 @@ root(isDark)
> .ext > .ext
opacity 0.5 opacity 0.5
.root.file[data-darkmode] .gvfdktuvdgwhmztnuekzkswkjygptfcv[data-darkmode]
root(true) root(true)
.root.file:not([data-darkmode]) .gvfdktuvdgwhmztnuekzkswkjygptfcv:not([data-darkmode])
root(false) root(false)
</style> </style>

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="root folder" <div class="ynntpczxvnusfwdyxsfuhvcmuypqopdd"
:data-is-contextmenu-showing="isContextmenuShowing" :data-is-contextmenu-showing="isContextmenuShowing"
:data-draghover="draghover" :data-draghover="draghover"
@click="onClick" @click="onClick"
@ -216,10 +216,10 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
@import '~const.styl' @import '~const.styl'
.root.folder root(isDark)
padding 8px padding 8px
height 64px height 64px
background lighten($theme-color, 95%) background isDark ? rgba($theme-color, 0.2) : lighten($theme-color, 95%)
border-radius 4px border-radius 4px
&, * &, *
@ -229,10 +229,10 @@ export default Vue.extend({
pointer-events none pointer-events none
&:hover &:hover
background lighten($theme-color, 90%) background isDark ? rgba(lighten($theme-color, 10%), 0.2) : lighten($theme-color, 90%)
&:active &:active
background lighten($theme-color, 85%) background isDark ? rgba(darken($theme-color, 10%), 0.2) : lighten($theme-color, 85%)
&[data-is-contextmenu-showing] &[data-is-contextmenu-showing]
&[data-draghover] &[data-draghover]
@ -248,16 +248,22 @@ export default Vue.extend({
border-radius 4px border-radius 4px
&[data-draghover] &[data-draghover]
background lighten($theme-color, 90%) background isDark ? rgba(darken($theme-color, 10%), 0.2) : lighten($theme-color, 90%)
> .name > .name
margin 0 margin 0
font-size 0.9em font-size 0.9em
color darken($theme-color, 30%) color isDark ? #fff : darken($theme-color, 30%)
> [data-fa] > [data-fa]
margin-right 4px margin-right 4px
margin-left 2px margin-left 2px
text-align left text-align left
.ynntpczxvnusfwdyxsfuhvcmuypqopdd[data-darkmode]
root(true)
.ynntpczxvnusfwdyxsfuhvcmuypqopdd:not([data-darkmode])
root(false)
</style> </style>

View file

@ -10,7 +10,10 @@
<span class="separator" v-if="folder != null">%fa:angle-right%</span> <span class="separator" v-if="folder != null">%fa:angle-right%</span>
<span class="folder current" v-if="folder != null">{{ folder.name }}</span> <span class="folder current" v-if="folder != null">{{ folder.name }}</span>
</div> </div>
<!--
TODO: #343
<input class="search" type="search" placeholder="&#xf002; %i18n:@search%"/> <input class="search" type="search" placeholder="&#xf002; %i18n:@search%"/>
-->
</nav> </nav>
<div class="main" :class="{ uploading: uploadings.length > 0, fetching }" <div class="main" :class="{ uploading: uploadings.length > 0, fetching }"
ref="main" ref="main"

View file

@ -1,7 +1,7 @@
<template> <template>
<mk-window width="400px" height="550px" @closed="$destroy"> <mk-window width="400px" height="550px" @closed="$destroy">
<span slot="header" :class="$style.header"> <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> </span>
<mk-followers :user="user"/> <mk-followers :user="user"/>
</mk-window> </mk-window>

View file

@ -1,7 +1,7 @@
<template> <template>
<mk-window width="400px" height="550px" @closed="$destroy"> <mk-window width="400px" height="550px" @closed="$destroy">
<span slot="header" :class="$style.header"> <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> </span>
<mk-following :user="user"/> <mk-following :user="user"/>
</mk-window> </mk-window>

View file

@ -34,7 +34,7 @@
</div> </div>
<div class="trash"> <div class="trash">
<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable> <x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable>
<p>ゴミ箱</p> <p>%i18n:common.trash%</p>
</div> </div>
</div> </div>
</div> </div>
@ -53,7 +53,7 @@
</div> </div>
</x-draggable> </x-draggable>
<div class="main"> <div class="main">
<a @click="hint">カスタマイズのヒント</a> <a @click="hint">%i18n:common.customization-tips.title%</a>
<div> <div>
<mk-post-form v-if="$store.state.settings.showPostFormOnTopOfTl"/> <mk-post-form v-if="$store.state.settings.showPostFormOnTopOfTl"/>
<mk-timeline ref="tl" @loaded="onTlLoaded"/> <mk-timeline ref="tl" @loaded="onTlLoaded"/>
@ -187,13 +187,13 @@ export default Vue.extend({
methods: { methods: {
hint() { hint() {
(this as any).apis.dialog({ (this as any).apis.dialog({
title: '%fa:info-circle%カスタマイズのヒント', title: '%fa:info-circle%%i18n:common.customization-tips.title%',
text: '<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' + text: '<p>%i18n:common.customization-tips.paragraph1%</p>' +
'<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' + '<p>%i18n:common.customization-tips.paragraph2%</p>' +
'<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' + '<p>%i18n:common.customization-tips.paragraph3%</p>' +
'<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>', '<p>%i18n:common.customization-tips.paragraph4%</p>',
actions: [{ actions: [{
text: 'Got it!' text: '%i18n:common.customization-tips.gotit%'
}] }]
}); });
}, },

View file

@ -1,5 +1,11 @@
<template> <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" :href="image.url"
@mousemove="onMousemove" @mousemove="onMousemove"
@mouseleave="onMouseleave" @mouseleave="onMouseleave"
@ -21,13 +27,17 @@ export default Vue.extend({
}, },
raw: { raw: {
default: false default: false
},
hide: {
type: Boolean,
default: true
} }
}, },
computed: { computed: {
style(): any { style(): any {
return { return {
'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent', '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> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.mk-media-image .lcjomzwbohoelkxsnuqjiaccdbdfiazy
display block display block
cursor zoom-in cursor zoom-in
overflow hidden overflow hidden
width 100% width 100%
height 100% height 100%
background-position center background-position center
border-radius 4px
&:not(:hover) &:not(:hover)
background-size cover 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> </style>

View file

@ -1,12 +1,19 @@
<template> <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" :src="video.url"
:title="video.name" :title="video.name"
controls controls
@dblclick.prevent="onClick" @dblclick.prevent="onClick"
ref="video" ref="video"
v-if="inlinePlayable" /> v-if="inlinePlayable" />
<a class="mk-media-video-thumbnail" <a class="thumbnail"
:href="video.url" :href="video.url"
:style="imageStyle" :style="imageStyle"
@click.prevent="onClick" @click.prevent="onClick"
@ -14,6 +21,7 @@
v-else> v-else>
%fa:R play-circle% %fa:R play-circle%
</a> </a>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -21,11 +29,23 @@ import Vue from 'vue';
import MkMediaVideoDialog from './media-video-dialog.vue'; import MkMediaVideoDialog from './media-video-dialog.vue';
export default Vue.extend({ export default Vue.extend({
props: ['video', 'inlinePlayable'], props: {
video: {
type: Object,
required: true
},
inlinePlayable: {
default: false
},
hide: {
type: Boolean,
default: true
}
},
computed: { computed: {
imageStyle(): any { imageStyle(): any {
return { return {
'background-image': `url(${this.video.url}?thumbnail&size=512)` 'background-image': `url(${this.video.url})`
}; };
} }
}, },
@ -47,13 +67,14 @@ export default Vue.extend({
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.mk-media-video .vwxdhznewyashiknzolsoihtlpicqepe
.video
display block display block
width 100% width 100%
height 100% height 100%
border-radius 4px border-radius 4px
.mk-media-video-thumbnail .thumbnail
display flex display flex
justify-content center justify-content center
align-items center align-items center
@ -65,4 +86,20 @@ export default Vue.extend({
background-size cover background-size cover
width 100% width 100%
height 100% height 100%
.uofhebxjdgksfmltszlxurtjnjjsvioh
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> </style>

View file

@ -8,7 +8,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { url } from '../../../config'; import { url } from '../../../config';
import getAcct from '../../../../../acct/render'; import getAcct from '../../../../../misc/acct/render';
export default Vue.extend({ export default Vue.extend({
props: ['user'], props: ['user'],

View file

@ -46,7 +46,7 @@
<mk-media-list :media-list="p.media" :raw="true"/> <mk-media-list :media-list="p.media" :raw="true"/>
</div> </div>
<mk-poll v-if="p.poll" :note="p"/> <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> <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="map" v-if="p.geo" ref="map"></div>
<div class="renote" v-if="p.renote"> <div class="renote" v-if="p.renote">

View file

@ -56,10 +56,10 @@
<button @click="menu" ref="menuButton"> <button @click="menu" ref="menuButton">
%fa:ellipsis-h% %fa:ellipsis-h%
</button> </button>
<button title="%i18n:@detail"> <!-- <button title="%i18n:@detail">
<template v-if="!isDetailOpened">%fa:caret-down%</template> <template v-if="!isDetailOpened">%fa:caret-down%</template>
<template v-if="isDetailOpened">%fa:caret-up%</template> <template v-if="isDetailOpened">%fa:caret-up%</template>
</button> </button> -->
</footer> </footer>
</div> </div>
</article> </article>

View file

@ -34,7 +34,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { url } from '../../../config'; import { url } from '../../../config';
import getNoteSummary from '../../../../../renderers/get-note-summary'; import getNoteSummary from '../../../../../misc/get-note-summary';
import XNote from './notes.note.vue'; import XNote from './notes.note.vue';

View file

@ -110,7 +110,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import getNoteSummary from '../../../../../renderers/get-note-summary'; import getNoteSummary from '../../../../../misc/get-note-summary';
export default Vue.extend({ export default Vue.extend({
data() { data() {

View file

@ -8,7 +8,11 @@
<div class="content"> <div class="content">
<div v-if="visibility == 'specified'" class="visibleUsers"> <div v-if="visibility == 'specified'" class="visibleUsers">
<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span> <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> </div>
<input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)"> <input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)">
<textarea :class="{ with: (files.length != 0 || poll) }" <textarea :class="{ with: (files.length != 0 || poll) }"
@ -19,7 +23,7 @@
<div class="medias" :class="{ with: poll }" v-show="files.length != 0"> <div class="medias" :class="{ with: poll }" v-show="files.length != 0">
<x-draggable :list="files" :options="{ animation: 150 }"> <x-draggable :list="files" :options="{ animation: 150 }">
<div v-for="file in files" :key="file.id"> <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=""/> <img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" title="%i18n:@attach-cancel%" alt=""/>
</div> </div>
</x-draggable> </x-draggable>
@ -32,9 +36,15 @@
<button class="drive" title="%i18n:@attach-media-from-drive%" @click="chooseFileFromDrive">%fa:cloud%</button> <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="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="%i18n:@create-poll%" @click="poll = true">%fa:chart-pie%</button>
<button class="poll" title="内容を隠す" @click="useCw = !useCw">%fa:eye-slash%</button> <button class="poll" title="%i18n:@hide-contents%" @click="useCw = !useCw">%fa:eye-slash%</button>
<button class="geo" title="位置情報を添付する" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> <button class="geo" title="%i18n:@attach-location-information%" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
<button class="visibility" title="公開範囲" @click="setVisibility" ref="visibilityButton">%fa:lock%</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> <p class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</p>
<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post"> <button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
{{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/> {{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
@ -46,6 +56,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import insertTextAtCursor from 'insert-text-at-cursor';
import * as XDraggable from 'vuedraggable'; import * as XDraggable from 'vuedraggable';
import getKao from '../../../common/scripts/get-kao'; import getKao from '../../../common/scripts/get-kao';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
@ -91,7 +102,8 @@ export default Vue.extend({
visibility: 'public', visibility: 'public',
visibleUsers: [], visibleUsers: [],
autocomplete: null, autocomplete: null,
draghover: false draghover: false,
recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]')
}; };
}, },
@ -131,7 +143,9 @@ export default Vue.extend({
}, },
canPost(): boolean { 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: { methods: {
addTag(tag: string) {
insertTextAtCursor(this.$refs.text, ` #${tag} `);
},
watch() { watch() {
this.$watch('text', () => this.saveDraft()); this.$watch('text', () => this.saveDraft());
this.$watch('poll', () => this.saveDraft()); this.$watch('poll', () => this.saveDraft());
@ -235,7 +253,7 @@ export default Vue.extend({
}, },
onKeydown(e) { 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) { onPaste(e) {
@ -287,7 +305,7 @@ export default Vue.extend({
setGeo() { setGeo() {
if (navigator.geolocation == null) { if (navigator.geolocation == null) {
alert('お使いの端末は位置情報に対応していません'); alert('%i18n:@geolocation-alert%');
return; return;
} }
@ -295,7 +313,7 @@ export default Vue.extend({
this.geo = pos.coords; this.geo = pos.coords;
this.$emit('geo-attached', this.geo); this.$emit('geo-attached', this.geo);
}, err => { }, err => {
alert('エラー: ' + err.message); alert('%i18n:@error%: ' + err.message);
}, { }, {
enableHighAccuracy: true enableHighAccuracy: true
}); });
@ -318,7 +336,7 @@ export default Vue.extend({
addVisibleUser() { addVisibleUser() {
(this as any).apis.input({ (this as any).apis.input({
title: 'ユーザー名を入力してください' title: '%i18n:@enter-username%'
}).then(username => { }).then(username => {
(this as any).api('users/show', { (this as any).api('users/show', {
username username
@ -370,6 +388,12 @@ export default Vue.extend({
}).then(() => { }).then(() => {
this.posting = false; 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() { saveDraft() {
@ -452,7 +476,7 @@ root(isDark)
margin 0 margin 0
max-width 100% max-width 100%
min-width 100% min-width 100%
min-height 64px min-height 84px
&:hover &:hover
& + * & + *
@ -478,6 +502,19 @@ root(isDark)
margin-right 16px margin-right 16px
color isDark ? #fff : #666 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 > .medias
margin 0 margin 0
padding 0 padding 0

View file

@ -2,7 +2,7 @@
<div class="profile"> <div class="profile">
<label class="avatar ui from group"> <label class="avatar ui from group">
<p>%i18n:@avatar%</p> <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> <button class="ui" @click="updateAvatar">%i18n:@choice-avatar%</button>
</label> </label>
<label class="ui from group"> <label class="ui from group">
@ -63,7 +63,7 @@ export default Vue.extend({
description: this.description || null, description: this.description || null,
birthday: this.birthday || null birthday: this.birthday || null
}).then(() => { }).then(() => {
(this as any).apis.notify('プロフィールを更新しました'); (this as any).apis.notify('%i18n:@profile-updated%');
}); });
}, },
onChangeIsLocked() { onChangeIsLocked() {

View file

@ -410,7 +410,7 @@ export default Vue.extend({
localStorage.clear(); localStorage.clear();
(this as any).apis.dialog({ (this as any).apis.dialog({
title: '%i18n:@cache-cleared%', title: '%i18n:@cache-cleared%',
text: '%i18n:@caache-cleared-desc%' text: '%i18n:@cache-cleared-desc%'
}); });
}, },
soundTest() { soundTest() {

Some files were not shown because too many files have changed in this diff Show more