Compare commits
122 Commits
Author | SHA1 | Date | |
---|---|---|---|
b74bf97761 | |||
a090b908bd | |||
3046821026 | |||
e94c73efe2 | |||
e85f9f4aa5 | |||
ad67886f96 | |||
5df0e102fd | |||
a04f0e3545 | |||
dff9c7ac48 | |||
3a80b59986 | |||
07560a4fdd | |||
7edca21c05 | |||
34105abd9d | |||
1bbca48a0b | |||
21f6a86772 | |||
6559197c55 | |||
05f9ad11bb | |||
f06d586680 | |||
4f45e8125c | |||
cc2843503d | |||
324a974dec | |||
4d4ffd70ac | |||
bf98a11b65 | |||
1117ce4b54 | |||
57e93b9b4e | |||
9e4b061ed0 | |||
1067bef7d6 | |||
8bff529acd | |||
4b08677839 | |||
70997cb551 | |||
bf0ef17e23 | |||
7dae5107f8 | |||
2dea88a147 | |||
f44c2a3e4f | |||
1fad3cbaae | |||
40d2e3e97c | |||
2efabe612e | |||
3107cbd6b9 | |||
3a061ed1c3 | |||
d4f0e6461a | |||
3285687652 | |||
51c53f64d0 | |||
1d582f5ad2 | |||
8a62748e39 | |||
6db3d6dfb6 | |||
38e2853dcf | |||
ba5a540ca3 | |||
fb1e05c2e9 | |||
aba84612a7 | |||
9bebbf4e03 | |||
e41b3f9c10 | |||
890dc05022 | |||
375f86ec82 | |||
db248a69c8 | |||
5951288159 | |||
17b92c9db2 | |||
962d1060d9 | |||
cb2640d961 | |||
29aeb0f082 | |||
990347f856 | |||
7a406c1f13 | |||
9432af2ab5 | |||
136b13e7ca | |||
ba1c823fb1 | |||
f1301a4780 | |||
7957cd4963 | |||
ee6590d03f | |||
f2a1238b20 | |||
e9ce84f368 | |||
52e84decb4 | |||
e893002bb6 | |||
3792103e80 | |||
7a861c9481 | |||
942b565224 | |||
88390d7a9a | |||
966fc4c5d7 | |||
84dbdf1196 | |||
211e7f90d9 | |||
e54b8e3fb2 | |||
836c89ed33 | |||
c7c73afea1 | |||
7b9ca63b1e | |||
c464183329 | |||
389f420cad | |||
6b2888383c | |||
3c38a867b4 | |||
7f5a69f4d8 | |||
bb9ab31d5e | |||
9def80af8a | |||
9256bcdbe4 | |||
9b775022bc | |||
32371ed2bd | |||
8b98c08a81 | |||
7cf72f7447 | |||
913385b10d | |||
7306468d08 | |||
11e5667778 | |||
38cc02e261 | |||
d52cf46cc1 | |||
c6110dd996 | |||
51d8de2c38 | |||
4455a1aa9d | |||
040d395ddb | |||
8296cac636 | |||
3eafe8b87d | |||
c01512e261 | |||
e5cf3aecd5 | |||
a8f90b41b7 | |||
b79169b975 | |||
437d52e2ed | |||
1329721440 | |||
6affb4fe97 | |||
15395686aa | |||
5cf1956135 | |||
fff307d4bb | |||
2b7782ba03 | |||
96d961ee80 | |||
9f064d76d9 | |||
2e39106c4b | |||
04650464f3 | |||
3bc7e1e35c | |||
7019ddbfc7 |
@ -60,11 +60,6 @@ mongodb:
|
||||
user: example-misskey-user
|
||||
pass: example-misskey-pass
|
||||
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
pass: example-pass
|
||||
|
||||
# Drive capacity of a local user (MB)
|
||||
localDriveCapacityMb: 256
|
||||
|
||||
@ -122,40 +117,50 @@ drive:
|
||||
# Below settings are optional
|
||||
#
|
||||
|
||||
# Redis
|
||||
#redis:
|
||||
# host: localhost
|
||||
# port: 6379
|
||||
# pass: example-pass
|
||||
|
||||
# Elasticsearch
|
||||
# elasticsearch:
|
||||
# host: localhost
|
||||
# port: 9200
|
||||
# pass: null
|
||||
#elasticsearch:
|
||||
# host: localhost
|
||||
# port: 9200
|
||||
# pass: null
|
||||
|
||||
# reCAPTCHA
|
||||
# recaptcha:
|
||||
# site_key: example-site-key
|
||||
#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
|
||||
#sw:
|
||||
# # Public key of VAPID
|
||||
# public_key: example-sw-public-key
|
||||
#
|
||||
# # Private key of VAPID
|
||||
# private_key: example-sw-private-key
|
||||
|
||||
# Twitter integration
|
||||
# You need to set the oauth callback url as : https://<your-misskey-instance>/api/tw/cb
|
||||
# twitter:
|
||||
# consumer_key: example-twitter-consumer-key
|
||||
# consumer_secret: example-twitter-consumer-secret-key
|
||||
#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
|
||||
#ghost: user-id-of-your-ghost-account
|
||||
|
||||
# Clustering
|
||||
# clusterLimit: 1
|
||||
#clusterLimit: 1
|
||||
|
||||
# Summaly proxy
|
||||
# summalyProxy: "http://example.com"
|
||||
#summalyProxy: "http://example.com"
|
||||
|
||||
# User recommendation
|
||||
#user_recommendation:
|
||||
# external: true
|
||||
# engine: http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}
|
||||
# timeout: 300000
|
||||
|
13
.config/mongo_initdb_example.js
Normal file
13
.config/mongo_initdb_example.js
Normal file
@ -0,0 +1,13 @@
|
||||
var user = {
|
||||
user: 'example-misskey-user',
|
||||
pwd: 'example-misskey-pass',
|
||||
roles: [
|
||||
{
|
||||
role: 'readWrite',
|
||||
db: 'misskey'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
db.createUser(user);
|
||||
|
12
.dockerignore
Executable file
12
.dockerignore
Executable file
@ -0,0 +1,12 @@
|
||||
.autogen
|
||||
.git
|
||||
.github
|
||||
.travis
|
||||
.vscode
|
||||
Dockerfile
|
||||
build/
|
||||
docker-compose.yml
|
||||
node_modules/
|
||||
mongo/
|
||||
redis/
|
||||
elasticsearch/
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
/.config/*
|
||||
!/.config/example.yml
|
||||
!/.config/mongo_initdb_example.js
|
||||
/.vscode
|
||||
/node_modules
|
||||
/build
|
||||
@ -12,3 +13,6 @@ npm-debug.log
|
||||
run.bat
|
||||
api-docs.json
|
||||
*.log
|
||||
/redis
|
||||
/mongo
|
||||
/elasticsearch
|
||||
|
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@ -0,0 +1,28 @@
|
||||
FROM alpine:latest AS base
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apk add --no-cache nodejs nodejs-npm
|
||||
RUN apk add vips fftw --update-cache --repository https://dl-3.alpinelinux.org/alpine/edge/testing/
|
||||
WORKDIR /misskey
|
||||
COPY . ./
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
RUN apk add --no-cache gcc g++ python autoconf automake file make nasm
|
||||
RUN apk add vips-dev fftw-dev --update-cache --repository https://dl-3.alpinelinux.org/alpine/edge/testing/
|
||||
RUN npm install \
|
||||
&& npm install -g node-gyp \
|
||||
&& node-gyp configure \
|
||||
&& node-gyp build \
|
||||
&& npm run build
|
||||
|
||||
FROM base AS runner
|
||||
|
||||
COPY --from=builder /misskey/built ./built
|
||||
COPY --from=builder /misskey/node_modules ./node_modules
|
||||
|
||||
RUN apk add --no-cache tini
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
|
||||
CMD ["npm", "start"]
|
@ -71,6 +71,7 @@ Please see [Contribution guide](./CONTRIBUTING.md).
|
||||
----------------------------------------------------------------
|
||||
<!-- PATREON_START -->
|
||||
<table><tr>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=CXe9AqlZy9AsYfiWd3OBYVOzvODoN47Litz0Tu4BFpU%3D" alt="Gargron"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12731202/0995c46cdcb54153ab5f073f5869b70a/1?token-time=2145916800&token-hash=Yd60FK_SWfQO56SeiJpy1tDHOnCV4xdEywQe8gn5_Wo%3D" alt="negao"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/13099460/43cecdbaa63a40d79bf50a96b9910b9d/1?token-time=2145916800&token-hash=d6P5MWHHsCMxUuBAEPAoVc5wLUR19mIhqAq7Ma9h9rI%3D" alt="ne_moni"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/2?token-time=2145916800&token-hash=mgPdX9TqZxEg4TTPuc477dxhIgYk9246qafjWZEqZ7g%3D" alt="Melilot"></td>
|
||||
@ -80,6 +81,7 @@ Please see [Contribution guide](./CONTRIBUTING.md).
|
||||
<td><img src="https://c8.patreon.com/2/100/12718187" alt="Peter G."></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/13039004/509d0c412eb14ae08d6a812a3054f7d6/1?token-time=2145916800&token-hash=zwSu01tOtn5xTUucDZHuPsCxF2HBEMVs9ROJKTlEV_o%3D" alt="nemu"></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://www.patreon.com/mastodon">Gargron</a></td>
|
||||
<td><a href="https://www.patreon.com/negao">negao</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=13099460">ne_moni</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td>
|
||||
|
52
docker-compose.yml
Normal file
52
docker-compose.yml
Normal file
@ -0,0 +1,52 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
restart: always
|
||||
links:
|
||||
- mongo
|
||||
- redis
|
||||
# - es
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000"
|
||||
networks:
|
||||
- internal_network
|
||||
- external_network
|
||||
|
||||
redis:
|
||||
restart: always
|
||||
image: redis:4.0-alpine
|
||||
networks:
|
||||
- internal_network
|
||||
### Uncomment to enable Redis persistance
|
||||
# volumes:
|
||||
# - ./redis:/data
|
||||
|
||||
mongo:
|
||||
restart: always
|
||||
image: mongo:4.1-bionic
|
||||
networks:
|
||||
- internal_network
|
||||
environment:
|
||||
MONGO_INITDB_DATABASE: "misskey"
|
||||
volumes:
|
||||
- ./.config/mongo_initdb.js:/docker-entrypoint-initdb.d/mongo_initdb.js:ro
|
||||
### Uncomment to enable MongoDB persistance
|
||||
# - ./mongo:/data
|
||||
|
||||
# es:
|
||||
# restart: always
|
||||
# image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.2
|
||||
# environment:
|
||||
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||
# networks:
|
||||
# - internal_network
|
||||
#### Uncomment to enable ES persistence
|
||||
## volumes:
|
||||
## - ./elasticsearch:/usr/share/elasticsearch/data
|
||||
|
||||
networks:
|
||||
internal_network:
|
||||
internal: true
|
||||
external_network:
|
47
docs/docker.en.md
Normal file
47
docs/docker.en.md
Normal file
@ -0,0 +1,47 @@
|
||||
Docker Guide
|
||||
================================================================
|
||||
|
||||
This guide describes how to install and setup Misskey with Docker.
|
||||
|
||||
[Japanese version also available - 日本語版もあります](./docker.ja.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* Make configuration files
|
||||
----------------------------------------------------------------
|
||||
1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`.
|
||||
2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` Copy the `.config/mongo_initdb_example.js` and rename it to `mongo_initdb.js`.
|
||||
2. Edit `default.yml` and `mongo_initdb.js`.
|
||||
|
||||
*2.* Configure Docker
|
||||
----------------------------------------------------------------
|
||||
Edit `docker-compose.yml`.
|
||||
|
||||
*3.* Build Misskey
|
||||
----------------------------------------------------------------
|
||||
Build misskey with the following:
|
||||
|
||||
`docker-compose build`
|
||||
|
||||
*4.* That is it.
|
||||
----------------------------------------------------------------
|
||||
Well done! Now, you have an environment that run to Misskey.
|
||||
|
||||
### Launch normally
|
||||
Just `docker-compose up -d`. GLHF!
|
||||
|
||||
### Way to Update to latest version of your Misskey
|
||||
1. `git fetch`
|
||||
2. `git stash`
|
||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
|
||||
4. `git stash pop`
|
||||
5. `docker-compose build`
|
||||
6. Check [ChangeLog](../CHANGELOG.md) for migration information
|
||||
7. `docker-compose stop && docker-compose up -d`
|
||||
|
||||
### Way to execute cli command:
|
||||
`docker-compose run --rm web node cli/mark-admin @example`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
If you have any questions or troubles, feel free to contact us!
|
48
docs/docker.ja.md
Normal file
48
docs/docker.ja.md
Normal file
@ -0,0 +1,48 @@
|
||||
Dockerを使ったMisskey構築方法
|
||||
================================================================
|
||||
|
||||
このガイドはDockerを使ったMisskeyセットアップ方法について解説します。
|
||||
|
||||
[英語版もあります - English version also available](./docker.en.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* 設定ファイルを作成する
|
||||
----------------------------------------------------------------
|
||||
1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする
|
||||
2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` `.config/mongo_initdb_example.js`をコピーし名前を`mongo_initdb.js`にする
|
||||
3. `default.yml`と`mongo_initdb.js`を編集する
|
||||
|
||||
*2.* Dockerの設定
|
||||
----------------------------------------------------------------
|
||||
`docker-compose.yml`を編集してください。
|
||||
|
||||
*3.* Misskeyのビルド
|
||||
----------------------------------------------------------------
|
||||
次のコマンドでMisskeyをビルドしてください:
|
||||
|
||||
`docker-compose build`
|
||||
|
||||
*4.* 以上です!
|
||||
----------------------------------------------------------------
|
||||
お疲れ様でした。これでMisskeyを動かす準備は整いました。
|
||||
|
||||
### 通常起動
|
||||
`docker-compose up -d`するだけです。GLHF!
|
||||
|
||||
### Misskeyを最新バージョンにアップデートする方法:
|
||||
1. `git fetch`
|
||||
2. `git stash`
|
||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
|
||||
4. `git stash pop`
|
||||
5. `docker-compose build`
|
||||
6. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する
|
||||
7. `docker-compose stop && docker-compose up -d`
|
||||
|
||||
### cliコマンドを実行する方法:
|
||||
|
||||
`docker-compose run --rm web node cli/mark-admin @example`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
なにかお困りのことがありましたらお気軽にご連絡ください。
|
@ -24,12 +24,12 @@ Please install and setup these softwares:
|
||||
#### Dependencies :package:
|
||||
* **[Node.js](https://nodejs.org/en/)**
|
||||
* **[MongoDB](https://www.mongodb.com/)** >= 3.6
|
||||
* **[Redis](https://redis.io/)**
|
||||
|
||||
##### Optional
|
||||
* [Redis](https://redis.io/)
|
||||
* Redis is optional, but we strongly recommended to install it
|
||||
* [Elasticsearch](https://www.elastic.co/) - used to provide searching feature instead of MongoDB
|
||||
|
||||
|
||||
*3.* Setup MongoDB
|
||||
----------------------------------------------------------------
|
||||
In root :
|
||||
|
@ -24,10 +24,17 @@ adduser --disabled-password --disabled-login misskey
|
||||
#### 依存関係 :package:
|
||||
* **[Node.js](https://nodejs.org/en/)**
|
||||
* **[MongoDB](https://www.mongodb.com/)** (3.6以上)
|
||||
* **[Redis](https://redis.io/)**
|
||||
|
||||
##### オプション
|
||||
* [Elasticsearch](https://www.elastic.co/) - 検索機能を向上させるために用います。
|
||||
* [Redis](https://redis.io/)
|
||||
* Redisはオプションですが、インストールすることを強く推奨します。
|
||||
* インストールしなくていいのは、あなたのインスタンスが自分専用のときだけとお考えください。
|
||||
* 具体的には、Redisをインストールしないと、次の事が出来なくなります:
|
||||
* Misskeyプロセスを複数起動しての負荷分散
|
||||
* レートリミット
|
||||
* Twitter連携
|
||||
* [Elasticsearch](https://www.elastic.co/)
|
||||
* 検索機能を有効にするためにはインストールが必要です。
|
||||
|
||||
*3.* MongoDBの設定
|
||||
----------------------------------------------------------------
|
||||
|
@ -286,6 +286,9 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
|
@ -286,6 +286,9 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
|
@ -286,6 +286,9 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
|
@ -286,6 +286,9 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
|
@ -83,17 +83,17 @@ common:
|
||||
pudding: "Pudding"
|
||||
note-visibility:
|
||||
public: "Public"
|
||||
home: "Accueil"
|
||||
home-desc: "Publier sur le fil local uniquement"
|
||||
home: "Principal"
|
||||
home-desc: "Publier sur le fil principal uniquement"
|
||||
followers: "Abonnés·es"
|
||||
followers-desc: "Publier à vos abonnés·es uniquement"
|
||||
specified: "Direct"
|
||||
specified-desc: "Publier aux utilisateurs·trices mentionnés·es"
|
||||
specified-desc: "Publier uniquement aux utilisateurs·rices mentionnés·es"
|
||||
private: "Privé"
|
||||
note-placeholders:
|
||||
a: "Que faites-vous maintenant ?"
|
||||
b: "Quoi de neuf ?"
|
||||
c: "Qu'avez-vous en tête ?"
|
||||
c: "Qu’avez-vous en tête ?"
|
||||
d: "Désirez-vous publier quelques mots ?"
|
||||
e: "Écrivez ici"
|
||||
f: "En attente de vos écrits"
|
||||
@ -103,7 +103,7 @@ common:
|
||||
ok: "OK"
|
||||
update-available-title: "Mise à jour disponible"
|
||||
update-available: "Une nouvelle version de Misskey est disponible ({newer}, version actuelle: {current}). Veuillez recharger la page pour appliquer la mise à jour."
|
||||
my-token-regenerated: "Votre token vient d'être généré, vous allez maintenant être déconnecté."
|
||||
my-token-regenerated: "Votre jeton vient d’être généré, vous allez maintenant être déconnecté."
|
||||
i-like-sushi: "Je préfère les sushis plutôt que le pudding"
|
||||
show-reversi-board-labels: "Afficher les étiquettes des lignes et colonnes dans Reversi"
|
||||
use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける"
|
||||
@ -120,7 +120,7 @@ common:
|
||||
my-turn: "C’est votre tour"
|
||||
opponent-turn: "Tour de l’adversaire"
|
||||
turn-of: "C’est le tour de {}"
|
||||
past-turn-of: "C'est au tour de {}"
|
||||
past-turn-of: "C’est au tour de {}"
|
||||
won: "{} a gagné"
|
||||
black: "Noirs"
|
||||
white: "Blancs"
|
||||
@ -267,8 +267,8 @@ common/views/components/media-banner.vue:
|
||||
common/views/components/theme.vue:
|
||||
light-theme: "非ダークモード時に使用するテーマ"
|
||||
dark-theme: "ダークモード時に使用するテーマ"
|
||||
light-themes: "明るいテーマ"
|
||||
dark-themes: "暗いテーマ"
|
||||
light-themes: "Thème clair"
|
||||
dark-themes: "Thème sombre"
|
||||
install-a-theme: "Installer un thème"
|
||||
theme-code: "Code du thème"
|
||||
install: "Installation"
|
||||
@ -286,13 +286,16 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "Thème n’est pas valide."
|
||||
already-installed: "Le thème est déjà installé."
|
||||
saved: "enregistré"
|
||||
manage-themes: "Gestion des thèmes"
|
||||
builtin-themes: "Thèmes standards"
|
||||
my-themes: "Mes thèmes"
|
||||
installed-themes: "Thèmes installés"
|
||||
select-theme: "Veuillez sélectionner un thème"
|
||||
uninstall: "Désinstaller"
|
||||
uninstalled: "« {} » a été désinstallé"
|
||||
author: "Auteur"
|
||||
desc: "Description"
|
||||
export: "エクスポート"
|
||||
export: "Exporter"
|
||||
import: "Importer"
|
||||
import-by-code: "Ou coller du code"
|
||||
theme-name-required: "Nom du thème est obligatoire."
|
||||
@ -326,8 +329,8 @@ common/views/components/nav.vue:
|
||||
wiki: "Wiki"
|
||||
donors: "Donateur·rice·s"
|
||||
repository: "Dépôt"
|
||||
develop: "Développeur·se·s"
|
||||
feedback: "Remarques"
|
||||
develop: "Développeurs"
|
||||
feedback: "Suggestions"
|
||||
common/views/components/note-menu.vue:
|
||||
detail: "Détails"
|
||||
copy-link: "Copier le lien"
|
||||
@ -407,10 +410,10 @@ common/views/components/visibility-chooser.vue:
|
||||
followers: "Abonné·e·s"
|
||||
followers-desc: "Publier à vos abonné·e·s uniquement"
|
||||
specified: "Direct"
|
||||
specified-desc: "Publier aux utilisateur·rice·s mentionné·e·s"
|
||||
specified-desc: "Publier uniquement aux utilisateurs·rices mentionné·e·s"
|
||||
private: "Privé"
|
||||
common/views/components/trends.vue:
|
||||
count: "{} utilisateurs·trices mentionnés·es"
|
||||
count: "{} utilisateurs·rices mentionnés·es"
|
||||
empty: "Aucune tendance"
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "Récupération"
|
||||
@ -511,7 +514,7 @@ desktop/views/components/charts.vue:
|
||||
notes: "投稿の増減 (統合)"
|
||||
local-notes: "投稿の増減 (ローカル)"
|
||||
remote-notes: "投稿の増減 (リモート)"
|
||||
notes-total: "投稿の積算"
|
||||
notes-total: "Total des notes"
|
||||
users: "Nombre d’utilisateurs·trices : augmentation/diminution"
|
||||
users-total: "ユーザーの積算"
|
||||
drive: "ドライブ使用量の増減"
|
||||
@ -855,7 +858,7 @@ desktop/views/components/timeline.vue:
|
||||
list-name: "Nom de la liste"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "Content de vous revoir !"
|
||||
adjective: "さん"
|
||||
adjective: "M."
|
||||
desktop/views/components/ui.header.account.vue:
|
||||
profile: "Votre profil"
|
||||
drive: "Drive"
|
||||
@ -908,9 +911,9 @@ desktop/views/pages/admin/admin.vue:
|
||||
desktop/views/pages/admin/admin.dashboard.vue:
|
||||
dashboard: "Tableau de bord"
|
||||
all-users: "Toutes les utilisateurrices"
|
||||
original-users: "Utilisateurrices sur cette instance"
|
||||
original-users: "Utilisateur·rice·s sur cette instance"
|
||||
all-notes: "Toutes les publications"
|
||||
original-notes: "Publication sur cette instance"
|
||||
original-notes: "Publications sur cette instance"
|
||||
invite: "Invitation"
|
||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||
suspend-user: "Suspendre un·e utilisateur·rice"
|
||||
@ -938,9 +941,9 @@ desktop/views/pages/deck/deck.note.vue:
|
||||
deleted: "cette publication a été supprimée"
|
||||
desktop/views/pages/stats/stats.vue:
|
||||
all-users: "Toutes les utilisateurrices"
|
||||
original-users: "Utilisateurrices sur cette instance"
|
||||
original-users: "Utilisateur·rice·s sur cette instance"
|
||||
all-notes: "Toutes les publications"
|
||||
original-notes: "Publication sur cette instance"
|
||||
original-notes: "Publications sur cette instance"
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "à propos"
|
||||
gotit: "J'ai compris !"
|
||||
@ -983,13 +986,13 @@ desktop/views/pages/user/user.followers-you-know.vue:
|
||||
loading: "Chargement en cours"
|
||||
no-users: "Pas d'utilisateurs"
|
||||
desktop/views/pages/user/user.friends.vue:
|
||||
title: "Personnes qui répondent le plus"
|
||||
title: "Mentions fréquentes"
|
||||
loading: "Chargement en cours"
|
||||
no-users: "Pas d'utilisateurs"
|
||||
desktop/views/pages/user/user.vue:
|
||||
is-suspended: "Ce compte a été suspendu."
|
||||
is-remote: "Cet utilisateur n'est pas un utilisateur de Misskey. Certaines informations peuvent être erronées"
|
||||
view-remote: "Voir les informations détaillées"
|
||||
is-remote: "Cet utilisateur n'est pas un utilisateur Misskey. Certaines informations peuvent ne pas refléter ce profil dans sa totalité."
|
||||
view-remote: "Consulter le profil complet"
|
||||
desktop/views/pages/user/user.home.vue:
|
||||
last-used-at: "Last used at"
|
||||
desktop/views/pages/user/user.photos.vue:
|
||||
@ -997,7 +1000,7 @@ desktop/views/pages/user/user.photos.vue:
|
||||
loading: "Chargement en cours"
|
||||
no-photos: "Pas de photos"
|
||||
desktop/views/pages/user/user.profile.vue:
|
||||
follows-you: "Vous suis"
|
||||
follows-you: "Vous suit"
|
||||
stalk: "Traquer"
|
||||
stalking: "ストーキングしています"
|
||||
unstalk: "ストーク解除"
|
||||
@ -1168,7 +1171,7 @@ mobile/views/pages/drive.vue:
|
||||
drive: "Drive"
|
||||
more: "Afficher plus ..."
|
||||
mobile/views/pages/signup.vue:
|
||||
lets-start: "Commençons ! 📦"
|
||||
lets-start: "Votre compte est prêt ! 📦"
|
||||
mobile/views/pages/followers.vue:
|
||||
followers-of: "Abonné·e·s de {}"
|
||||
mobile/views/pages/following.vue:
|
||||
@ -1283,7 +1286,7 @@ mobile/views/pages/settings.vue:
|
||||
sound: "Sons"
|
||||
enable-sounds: "Activer les sons"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "vous suit"
|
||||
follows-you: "Vous suit"
|
||||
following: "Abonnements"
|
||||
followers: "Abonné·e·s"
|
||||
notes: "Notes"
|
||||
@ -1291,8 +1294,8 @@ mobile/views/pages/user.vue:
|
||||
timeline: "Fil d'actualité"
|
||||
media: "Media"
|
||||
is-suspended: "This account has been suspended."
|
||||
is-remote: "Cet utilisateur n'est pas un utilisateur de Misskey. Certaines informations peuvent être erronées "
|
||||
view-remote: "Voir les informations détaillées"
|
||||
is-remote: "Ceci est le profil d’un utilisateur·rice distant·e. Certaines informations peuvent ne pas refléter ce profil dans sa totalité."
|
||||
view-remote: "Consulter son profil complet"
|
||||
mobile/views/pages/user/home.vue:
|
||||
recent-notes: "Notes récentes"
|
||||
images: "Images"
|
||||
@ -1316,7 +1319,7 @@ mobile/views/pages/user/home.photos.vue:
|
||||
no-photos: "Pas de photos"
|
||||
docs:
|
||||
edit-this-page-on-github: "Vous avez trouvé une erreur ou vous voulez contribuer à la documentation?"
|
||||
edit-this-page-on-github-link: "Modifiez cette page sur github!"
|
||||
edit-this-page-on-github-link: "Éditez cette page sur Github !"
|
||||
api:
|
||||
entities:
|
||||
properties: "Propriétés"
|
||||
|
@ -286,6 +286,9 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
|
@ -307,6 +307,9 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
|
@ -286,6 +286,9 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
|
@ -3,20 +3,20 @@ meta:
|
||||
lang: "한국어"
|
||||
divider: ""
|
||||
common:
|
||||
misskey: "A ⭐ of fediverse"
|
||||
about-title: "A ⭐ of fediverse."
|
||||
misskey: "연합우주의 ⭐"
|
||||
about-title: "연합우주의 ⭐."
|
||||
about: "Misskey를 찾아 주셔서 감사합니다. Misskey은 지구에서 태어난 <b>분산 마이크로 블로그 SNS </b> 입니다. Fediverse (다양한 SNS로 구성되는 우주)에 존재하는 다른 SNS와 상호 연결되어 있습니다. 잠시 도시의 번잡함에서 벗어나 새로운 인터넷에 다이브 해 보지 않겠습니까."
|
||||
intro:
|
||||
title: "Misskeyって?"
|
||||
title: "Misskey란?"
|
||||
about: "Misskeyはオープンソースの<b>分散型マイクロブログSNS</b>です。リッチで高度にカスタマイズできるUI、投稿へのリアクション、ファイルを一元管理できるドライブなど、先進的な機能を揃えています。また、Fediverseと呼ばれるネットワークに接続できるため、他のSNSともやり取りできます。例えば、あなたが何か投稿すると、その投稿はMisskeyだけでなく他のSNSにも伝わります。ちょうどある惑星から他の惑星に電波を発信している様子をイメージしてください。"
|
||||
features: "特徴"
|
||||
rich-contents: "投稿"
|
||||
features: "특징"
|
||||
rich-contents: "게시"
|
||||
rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。"
|
||||
reaction: "リアクション"
|
||||
reaction: "반응"
|
||||
reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
|
||||
ui: "インターフェース"
|
||||
ui: "인터페이스"
|
||||
ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。"
|
||||
drive: "ドライブ"
|
||||
drive: "드라이브"
|
||||
drive-desc: "以前投稿したことのある画像をまた投稿したくなったことはありませんか?もしくは、アップロードしたファイルをフォルダ分けして整理したくなったことはありませんか?Misskeyの根幹に組み込まれたドライブ機能によってそれらが解決します。ファイルの共有も簡単です。"
|
||||
outro: "他にもMisskeyにしかない機能はまだまだあるので、ぜひあなた自身の目で確かめてください。Misskeyは分散型SNSなので、このインスタンスが気に入らなければ他のインスタンスを試すこともできます。それでは、GLHF!"
|
||||
adblock:
|
||||
@ -71,7 +71,7 @@ common:
|
||||
friday: "금요일"
|
||||
saturday: "토요일"
|
||||
reactions:
|
||||
like: "いいね"
|
||||
like: "좋아요"
|
||||
love: "좋아"
|
||||
laugh: "크크"
|
||||
hmm: "음..."
|
||||
@ -82,14 +82,14 @@ common:
|
||||
rip: "RIP"
|
||||
pudding: "Pudding"
|
||||
note-visibility:
|
||||
public: "公開"
|
||||
home: "ホーム"
|
||||
home-desc: "ホームタイムラインにのみ公開"
|
||||
followers: "フォロワー"
|
||||
followers-desc: "自分のフォロワーにのみ公開"
|
||||
specified: "ダイレクト"
|
||||
specified-desc: "指定したユーザーにのみ公開"
|
||||
private: "非公開"
|
||||
public: "공개"
|
||||
home: "홈"
|
||||
home-desc: "홈 타임라인에만 공개"
|
||||
followers: "팔로워"
|
||||
followers-desc: "자신의 팔로워에게만 공개"
|
||||
specified: "다이렉트"
|
||||
specified-desc: "지정한 사용자에게만 공개"
|
||||
private: "비공개"
|
||||
note-placeholders:
|
||||
a: "지금 어떻게하고있어?"
|
||||
b: "뭔가 있었습니까?"
|
||||
@ -107,14 +107,14 @@ common:
|
||||
i-like-sushi: "나는(푸딩보다 오히려)스시가 좋아"
|
||||
show-reversi-board-labels: "리버시 보드의 행과 열 레이블을 표시"
|
||||
use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける"
|
||||
verified-user: "公式アカウント"
|
||||
verified-user: "공식 계정"
|
||||
disable-animated-mfm: "게시물의 문자 애니메이션을 비활성화 할"
|
||||
always-show-nsfw: "常に閲覧注意のメディアを表示する"
|
||||
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
|
||||
always-show-nsfw: "항상 열람주의 미디어를 표시"
|
||||
always-mark-nsfw: "항상 미디어를 열람주의로 설정하여 게시"
|
||||
show-full-acct: "ユーザー名のホストを省略しない"
|
||||
reduce-motion: "UIの動きを減らす"
|
||||
this-setting-is-this-device-only: "このデバイスのみ"
|
||||
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
|
||||
this-setting-is-this-device-only: "이 장치만"
|
||||
do-not-use-in-production: '이것은 개발 빌드입니다. 프로덕션 환경에서 사용하지 마십시오.'
|
||||
reversi:
|
||||
drawn: "무승부"
|
||||
my-turn: "당신의 차례입니다"
|
||||
@ -155,19 +155,19 @@ common:
|
||||
home: "홈"
|
||||
local: "로컬"
|
||||
hybrid: "소셜"
|
||||
hashtag: "ハッシュタグ"
|
||||
hashtag: "해시태그"
|
||||
global: "글로벌"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト投稿"
|
||||
notifications: "통지"
|
||||
list: "목록"
|
||||
swap-left: "左に移動"
|
||||
swap-right: "右に移動"
|
||||
swap-up: "上に移動"
|
||||
swap-down: "下に移動"
|
||||
remove: "カラムを削除"
|
||||
add-column: "カラムを追加"
|
||||
rename: "名前を変更"
|
||||
swap-left: "왼쪽으로 이동"
|
||||
swap-right: "오른쪽으로 이동"
|
||||
swap-up: "위로 이동"
|
||||
swap-down: "아래로 이동"
|
||||
remove: "칼럼 제거"
|
||||
add-column: "칼럼 추가"
|
||||
rename: "이름 변경"
|
||||
stack-left: "左に重ねる"
|
||||
pop-right: "右に出す"
|
||||
auth/views/form.vue:
|
||||
@ -248,44 +248,47 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
checking-network: "ネットワーク接続を確認中"
|
||||
internet: "インターネット接続"
|
||||
checking-internet: "インターネット接続を確認中"
|
||||
server: "サーバー接続"
|
||||
server: "서버 연결"
|
||||
checking-server: "サーバー接続を確認中"
|
||||
finding: "問題を調べています"
|
||||
no-network: "ネットワークに接続されていません"
|
||||
no-network-desc: "お使いのPCのネットワーク接続が正常か確認してください。"
|
||||
no-internet: "インターネットに接続されていません"
|
||||
no-internet-desc: "ネットワークには接続されていますが、インターネットには接続されていないようです。お使いのPCのインターネット接続が正常か確認してください。"
|
||||
no-server: "Misskeyのサーバーに接続できません"
|
||||
no-server: "Misskey 서버에 연결할 수 없습니다."
|
||||
no-server-desc: "お使いのPCのインターネット接続は正常ですが、Misskeyのサーバーには接続できませんでした。サーバーがダウンまたはメンテナンスしている可能性があるので、しばらくしてから再度御アクセスください。"
|
||||
success: "Misskeyのサーバーに接続できました"
|
||||
success-desc: "正常に接続できるようです。ページを再度読み込みしてください。"
|
||||
flush: "キャッシュの削除"
|
||||
set-version: "バージョン指定"
|
||||
flush: "캐시 삭제"
|
||||
set-version: "버전 지정"
|
||||
common/views/components/media-banner.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
sensitive: "열람주의"
|
||||
click-to-show: "클릭하여 표시"
|
||||
common/views/components/theme.vue:
|
||||
light-theme: "非ダークモード時に使用するテーマ"
|
||||
dark-theme: "ダークモード時に使用するテーマ"
|
||||
light-themes: "明るいテーマ"
|
||||
dark-themes: "暗いテーマ"
|
||||
install-a-theme: "テーマのインストール"
|
||||
theme-code: "テーマコード"
|
||||
install: "インストール"
|
||||
light-themes: "밝은 테마"
|
||||
dark-themes: "어두운 테마"
|
||||
install-a-theme: "테마 설치"
|
||||
theme-code: "테마 코드"
|
||||
install: "설치"
|
||||
installed: "「{}」をインストールしました"
|
||||
create-a-theme: "テーマの作成"
|
||||
save-created-theme: "テーマを保存"
|
||||
primary-color: "プライマリ カラー"
|
||||
secondary-color: "セカンダリ カラー"
|
||||
text-color: "文字色"
|
||||
create-a-theme: "테마 만들기"
|
||||
save-created-theme: "테마 저장"
|
||||
primary-color: "기본 색"
|
||||
secondary-color: "보조 색"
|
||||
text-color: "글자 색상"
|
||||
base-theme: "ベーステーマ"
|
||||
base-theme-light: "Light"
|
||||
base-theme-dark: "Dark"
|
||||
theme-name: "テーマ名"
|
||||
preview-created-theme: "プレビュー"
|
||||
theme-name: "테마명"
|
||||
preview-created-theme: "미리보기"
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
@ -305,7 +308,7 @@ common/views/components/messaging.vue:
|
||||
no-history: "履歴はありません"
|
||||
common/views/components/messaging-room.vue:
|
||||
empty: "このユーザーと話したことはありません"
|
||||
more: "もっと読む"
|
||||
more: "더 보기"
|
||||
no-history: "これより過去の履歴はありません"
|
||||
resize-form: "ドラッグしてフォームの広さを調整"
|
||||
new-message: "新しいメッセージがあります"
|
||||
@ -320,17 +323,17 @@ common/views/components/messaging-room.message.vue:
|
||||
is-read: "읽음"
|
||||
deleted: "このメッセージは削除されました"
|
||||
common/views/components/nav.vue:
|
||||
about: "Misskeyについて"
|
||||
stats: "統計"
|
||||
about: "Misskey에 대하여"
|
||||
stats: "통계"
|
||||
status: "ステータス"
|
||||
wiki: "Wiki"
|
||||
donors: "ドナー"
|
||||
repository: "リポジトリ"
|
||||
develop: "開発者"
|
||||
feedback: "フィードバック"
|
||||
donors: "기증자"
|
||||
repository: "저장소"
|
||||
develop: "개발자"
|
||||
feedback: "피드백"
|
||||
common/views/components/note-menu.vue:
|
||||
detail: "詳細"
|
||||
copy-link: "リンクをコピー"
|
||||
copy-link: "링크 복사"
|
||||
favorite: "お気に入り"
|
||||
pin: "ピン留め"
|
||||
unpin: "ピン留め解除"
|
||||
|
@ -286,6 +286,9 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
|
@ -286,6 +286,9 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
|
@ -286,6 +286,9 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
|
@ -286,6 +286,9 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
|
@ -286,6 +286,9 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
|
@ -286,6 +286,9 @@ common/views/components/theme.vue:
|
||||
invalid-theme: "テーマが正しくありません。"
|
||||
already-installed: "既にそのテーマはインストールされています。"
|
||||
saved: "保存しました"
|
||||
manage-themes: "テーマの管理"
|
||||
builtin-themes: "標準テーマ"
|
||||
my-themes: "マイテーマ"
|
||||
installed-themes: "インストールされたテーマ"
|
||||
select-theme: "テーマを選択してください"
|
||||
uninstall: "アンインストール"
|
||||
|
28
package.json
28
package.json
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "10.0.0",
|
||||
"clientVersion": "1.0.10328",
|
||||
"version": "10.9.2",
|
||||
"clientVersion": "1.0.10448",
|
||||
"codename": "nighthike",
|
||||
"main": "./built/index.js",
|
||||
"private": true,
|
||||
@ -48,7 +48,7 @@
|
||||
"@types/koa-bodyparser": "5.0.1",
|
||||
"@types/koa-compress": "2.0.8",
|
||||
"@types/koa-favicon": "2.0.19",
|
||||
"@types/koa-logger": "3.1.0",
|
||||
"@types/koa-logger": "3.1.1",
|
||||
"@types/koa-mount": "3.0.1",
|
||||
"@types/koa-multer": "1.0.0",
|
||||
"@types/koa-router": "7.0.32",
|
||||
@ -58,14 +58,14 @@
|
||||
"@types/minio": "7.0.0",
|
||||
"@types/mkdirp": "0.5.2",
|
||||
"@types/mocha": "5.2.3",
|
||||
"@types/mongodb": "3.1.10",
|
||||
"@types/mongodb": "3.1.12",
|
||||
"@types/ms": "0.7.30",
|
||||
"@types/node": "10.11.4",
|
||||
"@types/node": "10.11.7",
|
||||
"@types/portscanner": "2.1.0",
|
||||
"@types/pug": "2.0.4",
|
||||
"@types/qrcode": "1.3.0",
|
||||
"@types/ratelimiter": "2.1.28",
|
||||
"@types/redis": "2.8.6",
|
||||
"@types/redis": "2.8.7",
|
||||
"@types/request": "2.47.1",
|
||||
"@types/request-promise-native": "1.0.15",
|
||||
"@types/rimraf": "2.0.2",
|
||||
@ -78,7 +78,7 @@
|
||||
"@types/tinycolor2": "1.4.1",
|
||||
"@types/tmp": "0.0.33",
|
||||
"@types/uuid": "3.4.4",
|
||||
"@types/webpack": "4.4.14",
|
||||
"@types/webpack": "4.4.16",
|
||||
"@types/webpack-stream": "3.2.10",
|
||||
"@types/websocket": "0.0.40",
|
||||
"@types/ws": "6.0.1",
|
||||
@ -92,11 +92,11 @@
|
||||
"cafy": "11.3.0",
|
||||
"chalk": "2.4.1",
|
||||
"chart.js": "2.7.2",
|
||||
"commander": "2.17.1",
|
||||
"commander": "2.19.0",
|
||||
"crc-32": "1.2.0",
|
||||
"css-loader": "1.0.0",
|
||||
"dateformat": "3.0.3",
|
||||
"debug": "4.0.1",
|
||||
"debug": "4.1.0",
|
||||
"deep-equal": "1.0.1",
|
||||
"deepcopy": "0.6.3",
|
||||
"diskusage": "0.2.5",
|
||||
@ -169,6 +169,7 @@
|
||||
"parse5": "5.1.0",
|
||||
"portscanner": "2.2.0",
|
||||
"progress-bar-webpack-plugin": "1.11.0",
|
||||
"promise-limit": "2.7.0",
|
||||
"promise-sequential": "1.1.1",
|
||||
"pug": "2.0.3",
|
||||
"punycode": "2.1.1",
|
||||
@ -191,7 +192,7 @@
|
||||
"single-line-log": "1.1.2",
|
||||
"speakeasy": "2.0.0",
|
||||
"stringz": "1.0.0",
|
||||
"style-loader": "0.23.0",
|
||||
"style-loader": "0.23.1",
|
||||
"stylus": "0.54.5",
|
||||
"stylus-loader": "3.0.2",
|
||||
"summaly": "2.2.0",
|
||||
@ -204,14 +205,14 @@
|
||||
"ts-node": "7.0.1",
|
||||
"tslint": "5.10.0",
|
||||
"typescript": "2.9.2",
|
||||
"typescript-eslint-parser": "19.0.2",
|
||||
"typescript-eslint-parser": "20.0.0",
|
||||
"uglify-es": "3.3.9",
|
||||
"url-loader": "1.1.1",
|
||||
"url-loader": "1.1.2",
|
||||
"uuid": "3.3.2",
|
||||
"v-animate-css": "0.0.2",
|
||||
"vue": "2.5.17",
|
||||
"vue-chartjs": "3.4.0",
|
||||
"vue-color": "2.6.0",
|
||||
"vue-color": "2.7.0",
|
||||
"vue-cropperjs": "2.2.2",
|
||||
"vue-js-modal": "1.3.26",
|
||||
"vue-json-tree-view": "2.1.4",
|
||||
@ -219,6 +220,7 @@
|
||||
"vue-router": "3.0.1",
|
||||
"vue-style-loader": "4.1.2",
|
||||
"vue-svg-inline-loader": "1.2.0",
|
||||
"vue-sweetalert2": "1.5.5",
|
||||
"vue-template-compiler": "2.5.17",
|
||||
"vuedraggable": "2.16.0",
|
||||
"vuewordcloud": "18.7.11",
|
||||
|
@ -130,3 +130,29 @@ pre
|
||||
|
||||
[data-fa]
|
||||
display inline-block
|
||||
|
||||
.swal2-container
|
||||
z-index 10000 !important
|
||||
|
||||
&.swal2-shown
|
||||
background-color rgba(0, 0, 0, 0.5) !important
|
||||
|
||||
.swal2-popup
|
||||
background var(--face) !important
|
||||
|
||||
.swal2-content
|
||||
color var(--text) !important
|
||||
|
||||
.swal2-confirm
|
||||
background-color var(--primary) !important
|
||||
border-left-color var(--primary) !important
|
||||
border-right-color var(--primary) !important
|
||||
color var(--primaryForeground) !important
|
||||
|
||||
&:hover
|
||||
background-image none !important
|
||||
background-color var(--primaryDarken5) !important
|
||||
|
||||
&:active
|
||||
background-image none !important
|
||||
background-color var(--primaryDarken5) !important
|
||||
|
@ -13,14 +13,14 @@ export default prop => ({
|
||||
},
|
||||
|
||||
$_ns_isRenote(): boolean {
|
||||
return (this.$_ns_note_.renote &&
|
||||
return (this.$_ns_note_.renote != null &&
|
||||
this.$_ns_note_.text == null &&
|
||||
this.$_ns_note_.fileIds.length == 0 &&
|
||||
this.$_ns_note_.poll == null);
|
||||
},
|
||||
|
||||
$_ns_target(): any {
|
||||
return this._ns_isRenote ? this.$_ns_note_.renote : this.$_ns_note_;
|
||||
return this.$_ns_isRenote ? this.$_ns_note_.renote : this.$_ns_note_;
|
||||
},
|
||||
},
|
||||
|
||||
@ -86,8 +86,20 @@ export default prop => ({
|
||||
switch (type) {
|
||||
case 'reacted': {
|
||||
const reaction = body.reaction;
|
||||
if (this.$_ns_target.reactionCounts == null) Vue.set(this.$_ns_target, 'reactionCounts', {});
|
||||
this.$_ns_target.reactionCounts[reaction] = (this.$_ns_target.reactionCounts[reaction] || 0) + 1;
|
||||
|
||||
if (this.$_ns_target.reactionCounts == null) {
|
||||
Vue.set(this.$_ns_target, 'reactionCounts', {});
|
||||
}
|
||||
|
||||
if (this.$_ns_target.reactionCounts[reaction] == null) {
|
||||
Vue.set(this.$_ns_target.reactionCounts, reaction, 0);
|
||||
}
|
||||
|
||||
this.$_ns_target.reactionCounts[reaction]++;
|
||||
|
||||
if (body.userId == this.$store.state.i.id) {
|
||||
Vue.set(this.$_ns_target, 'myReaction', reaction);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ import MiOS from '../../mios';
|
||||
export default class Stream extends EventEmitter {
|
||||
private stream: ReconnectingWebsocket;
|
||||
private state: string;
|
||||
private buffer: any[];
|
||||
private sharedConnectionPools: Pool[] = [];
|
||||
private sharedConnections: SharedConnection[] = [];
|
||||
private nonSharedConnections: NonSharedConnection[] = [];
|
||||
|
||||
@ -18,7 +18,6 @@ export default class Stream extends EventEmitter {
|
||||
super();
|
||||
|
||||
this.state = 'initializing';
|
||||
this.buffer = [];
|
||||
|
||||
const user = os.store.state.i;
|
||||
|
||||
@ -26,114 +25,34 @@ export default class Stream extends EventEmitter {
|
||||
this.stream.addEventListener('open', this.onOpen);
|
||||
this.stream.addEventListener('close', this.onClose);
|
||||
this.stream.addEventListener('message', this.onMessage);
|
||||
|
||||
if (user) {
|
||||
const main = this.useSharedConnection('main');
|
||||
|
||||
// 自分の情報が更新されたとき
|
||||
main.on('meUpdated', i => {
|
||||
os.store.dispatch('mergeMe', i);
|
||||
});
|
||||
|
||||
main.on('readAllNotifications', () => {
|
||||
os.store.dispatch('mergeMe', {
|
||||
hasUnreadNotification: false
|
||||
});
|
||||
});
|
||||
|
||||
main.on('unreadNotification', () => {
|
||||
os.store.dispatch('mergeMe', {
|
||||
hasUnreadNotification: true
|
||||
});
|
||||
});
|
||||
|
||||
main.on('readAllMessagingMessages', () => {
|
||||
os.store.dispatch('mergeMe', {
|
||||
hasUnreadMessagingMessage: false
|
||||
});
|
||||
});
|
||||
|
||||
main.on('unreadMessagingMessage', () => {
|
||||
os.store.dispatch('mergeMe', {
|
||||
hasUnreadMessagingMessage: true
|
||||
});
|
||||
});
|
||||
|
||||
main.on('unreadMention', () => {
|
||||
os.store.dispatch('mergeMe', {
|
||||
hasUnreadMentions: true
|
||||
});
|
||||
});
|
||||
|
||||
main.on('readAllUnreadMentions', () => {
|
||||
os.store.dispatch('mergeMe', {
|
||||
hasUnreadMentions: false
|
||||
});
|
||||
});
|
||||
|
||||
main.on('unreadSpecifiedNote', () => {
|
||||
os.store.dispatch('mergeMe', {
|
||||
hasUnreadSpecifiedNotes: true
|
||||
});
|
||||
});
|
||||
|
||||
main.on('readAllUnreadSpecifiedNotes', () => {
|
||||
os.store.dispatch('mergeMe', {
|
||||
hasUnreadSpecifiedNotes: false
|
||||
});
|
||||
});
|
||||
|
||||
main.on('clientSettingUpdated', x => {
|
||||
os.store.commit('settings/set', {
|
||||
key: x.key,
|
||||
value: x.value
|
||||
});
|
||||
});
|
||||
|
||||
main.on('homeUpdated', x => {
|
||||
os.store.commit('settings/setHome', x);
|
||||
});
|
||||
|
||||
main.on('mobileHomeUpdated', x => {
|
||||
os.store.commit('settings/setMobileHome', x);
|
||||
});
|
||||
|
||||
main.on('widgetUpdated', x => {
|
||||
os.store.commit('settings/setWidget', {
|
||||
id: x.id,
|
||||
data: x.data
|
||||
});
|
||||
});
|
||||
|
||||
// トークンが再生成されたとき
|
||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||
main.on('myTokenRegenerated', () => {
|
||||
alert('%i18n:common.my-token-regenerated%');
|
||||
os.signout();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public useSharedConnection = (channel: string): SharedConnection => {
|
||||
const existConnection = this.sharedConnections.find(c => c.channel === channel);
|
||||
@autobind
|
||||
public useSharedConnection(channel: string): SharedConnection {
|
||||
let pool = this.sharedConnectionPools.find(p => p.channel === channel);
|
||||
|
||||
if (existConnection) {
|
||||
existConnection.use();
|
||||
return existConnection;
|
||||
} else {
|
||||
const connection = new SharedConnection(this, channel);
|
||||
connection.use();
|
||||
this.sharedConnections.push(connection);
|
||||
return connection;
|
||||
if (pool == null) {
|
||||
pool = new Pool(this, channel);
|
||||
this.sharedConnectionPools.push(pool);
|
||||
}
|
||||
|
||||
const connection = new SharedConnection(this, channel, pool);
|
||||
this.sharedConnections.push(connection);
|
||||
return connection;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public removeSharedConnection(connection: SharedConnection) {
|
||||
this.sharedConnections = this.sharedConnections.filter(c => c.id !== connection.id);
|
||||
this.sharedConnections = this.sharedConnections.filter(c => c !== connection);
|
||||
}
|
||||
|
||||
public connectToChannel = (channel: string, params?: any): NonSharedConnection => {
|
||||
@autobind
|
||||
public removeSharedConnectionPool(pool: Pool) {
|
||||
this.sharedConnectionPools = this.sharedConnectionPools.filter(p => p !== pool);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public connectToChannel(channel: string, params?: any): NonSharedConnection {
|
||||
const connection = new NonSharedConnection(this, channel, params);
|
||||
this.nonSharedConnections.push(connection);
|
||||
return connection;
|
||||
@ -141,7 +60,7 @@ export default class Stream extends EventEmitter {
|
||||
|
||||
@autobind
|
||||
public disconnectToChannel(connection: NonSharedConnection) {
|
||||
this.nonSharedConnections = this.nonSharedConnections.filter(c => c.id !== connection.id);
|
||||
this.nonSharedConnections = this.nonSharedConnections.filter(c => c !== connection);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,17 +73,10 @@ export default class Stream extends EventEmitter {
|
||||
this.state = 'connected';
|
||||
this.emit('_connected_');
|
||||
|
||||
// バッファーを処理
|
||||
const _buffer = [].concat(this.buffer); // Shallow copy
|
||||
this.buffer = []; // Clear buffer
|
||||
_buffer.forEach(data => {
|
||||
this.send(data); // Resend each buffered messages
|
||||
});
|
||||
|
||||
// チャンネル再接続
|
||||
if (isReconnect) {
|
||||
this.sharedConnections.forEach(c => {
|
||||
c.connect();
|
||||
this.sharedConnectionPools.forEach(p => {
|
||||
p.connect();
|
||||
});
|
||||
this.nonSharedConnections.forEach(c => {
|
||||
c.connect();
|
||||
@ -177,8 +89,10 @@ export default class Stream extends EventEmitter {
|
||||
*/
|
||||
@autobind
|
||||
private onClose() {
|
||||
this.state = 'reconnecting';
|
||||
this.emit('_disconnected_');
|
||||
if (this.state == 'connected') {
|
||||
this.state = 'reconnecting';
|
||||
this.emit('_disconnected_');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -190,8 +104,18 @@ export default class Stream extends EventEmitter {
|
||||
|
||||
if (type == 'channel') {
|
||||
const id = body.id;
|
||||
const connection = this.sharedConnections.find(c => c.id === id) || this.nonSharedConnections.find(c => c.id === id);
|
||||
connection.emit(body.type, body.body);
|
||||
|
||||
let connections: Connection[];
|
||||
|
||||
connections = this.sharedConnections.filter(c => c.id === id);
|
||||
|
||||
if (connections.length === 0) {
|
||||
connections = [this.nonSharedConnections.find(c => c.id === id)];
|
||||
}
|
||||
|
||||
connections.filter(c => c != null).forEach(c => {
|
||||
c.emit(body.type, body.body);
|
||||
});
|
||||
} else {
|
||||
this.emit(type, body);
|
||||
}
|
||||
@ -207,12 +131,6 @@ export default class Stream extends EventEmitter {
|
||||
body: payload
|
||||
};
|
||||
|
||||
// まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
|
||||
if (this.state != 'connected') {
|
||||
this.buffer.push(data);
|
||||
return;
|
||||
}
|
||||
|
||||
this.stream.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
@ -226,19 +144,139 @@ export default class Stream extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Connection extends EventEmitter {
|
||||
class Pool {
|
||||
public channel: string;
|
||||
public id: string;
|
||||
protected params: any;
|
||||
protected stream: Stream;
|
||||
public users = 0;
|
||||
private disposeTimerId: any;
|
||||
private isConnected = false;
|
||||
|
||||
constructor(stream: Stream, channel: string, params?: any) {
|
||||
constructor(stream: Stream, channel: string) {
|
||||
this.channel = channel;
|
||||
this.stream = stream;
|
||||
|
||||
this.id = Math.random().toString();
|
||||
|
||||
this.stream.on('_disconnected_', this.onStreamDisconnected);
|
||||
}
|
||||
|
||||
@autobind
|
||||
private onStreamDisconnected() {
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public inc() {
|
||||
if (this.users === 0 && !this.isConnected) {
|
||||
this.connect();
|
||||
}
|
||||
|
||||
this.users++;
|
||||
|
||||
// タイマー解除
|
||||
if (this.disposeTimerId) {
|
||||
clearTimeout(this.disposeTimerId);
|
||||
this.disposeTimerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public dec() {
|
||||
this.users--;
|
||||
|
||||
// そのコネクションの利用者が誰もいなくなったら
|
||||
if (this.users === 0) {
|
||||
// また直ぐに再利用される可能性があるので、一定時間待ち、
|
||||
// 新たな利用者が現れなければコネクションを切断する
|
||||
this.disposeTimerId = setTimeout(() => {
|
||||
this.disconnect();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public connect() {
|
||||
if (this.isConnected) return;
|
||||
this.isConnected = true;
|
||||
this.stream.send('connect', {
|
||||
channel: this.channel,
|
||||
id: this.id
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
private disconnect() {
|
||||
this.stream.off('_disconnected_', this.onStreamDisconnected);
|
||||
this.stream.send('disconnect', { id: this.id });
|
||||
this.stream.removeSharedConnectionPool(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Connection extends EventEmitter {
|
||||
public channel: string;
|
||||
protected stream: Stream;
|
||||
public abstract id: string;
|
||||
|
||||
constructor(stream: Stream, channel: string) {
|
||||
super();
|
||||
|
||||
this.stream = stream;
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public send(id: string, typeOrPayload, payload?) {
|
||||
const type = payload === undefined ? typeOrPayload.type : typeOrPayload;
|
||||
const body = payload === undefined ? typeOrPayload.body : payload;
|
||||
|
||||
this.stream.send('ch', {
|
||||
id: id,
|
||||
type: type,
|
||||
body: body
|
||||
});
|
||||
}
|
||||
|
||||
public abstract dispose(): void;
|
||||
}
|
||||
|
||||
class SharedConnection extends Connection {
|
||||
private pool: Pool;
|
||||
|
||||
public get id(): string {
|
||||
return this.pool.id;
|
||||
}
|
||||
|
||||
constructor(stream: Stream, channel: string, pool: Pool) {
|
||||
super(stream, channel);
|
||||
|
||||
this.pool = pool;
|
||||
this.pool.inc();
|
||||
}
|
||||
|
||||
@autobind
|
||||
public send(typeOrPayload, payload?) {
|
||||
super.send(this.pool.id, typeOrPayload, payload);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public dispose() {
|
||||
this.pool.dec();
|
||||
this.removeAllListeners();
|
||||
this.stream.removeSharedConnection(this);
|
||||
}
|
||||
}
|
||||
|
||||
class NonSharedConnection extends Connection {
|
||||
public id: string;
|
||||
protected params: any;
|
||||
|
||||
constructor(stream: Stream, channel: string, params?: any) {
|
||||
super(stream, channel);
|
||||
|
||||
this.params = params;
|
||||
this.id = Math.random().toString();
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
@ -253,60 +291,7 @@ abstract class Connection extends EventEmitter {
|
||||
|
||||
@autobind
|
||||
public send(typeOrPayload, payload?) {
|
||||
const data = payload === undefined ? typeOrPayload : {
|
||||
type: typeOrPayload,
|
||||
body: payload
|
||||
};
|
||||
|
||||
this.stream.send('channel', {
|
||||
id: this.id,
|
||||
body: data
|
||||
});
|
||||
}
|
||||
|
||||
public abstract dispose: () => void;
|
||||
}
|
||||
|
||||
class SharedConnection extends Connection {
|
||||
private users = 0;
|
||||
private disposeTimerId: any;
|
||||
|
||||
constructor(stream: Stream, channel: string) {
|
||||
super(stream, channel);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public use() {
|
||||
this.users++;
|
||||
|
||||
// タイマー解除
|
||||
if (this.disposeTimerId) {
|
||||
clearTimeout(this.disposeTimerId);
|
||||
this.disposeTimerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public dispose() {
|
||||
this.users--;
|
||||
|
||||
// そのコネクションの利用者が誰もいなくなったら
|
||||
if (this.users === 0) {
|
||||
// また直ぐに再利用される可能性があるので、一定時間待ち、
|
||||
// 新たな利用者が現れなければコネクションを切断する
|
||||
this.disposeTimerId = setTimeout(() => {
|
||||
this.disposeTimerId = null;
|
||||
this.removeAllListeners();
|
||||
this.stream.send('disconnect', { id: this.id });
|
||||
this.stream.removeSharedConnection(this);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NonSharedConnection extends Connection {
|
||||
constructor(stream: Stream, channel: string, params?: any) {
|
||||
super(stream, channel, params);
|
||||
super.send(this.id, typeOrPayload, payload);
|
||||
}
|
||||
|
||||
@autobind
|
||||
|
@ -186,9 +186,8 @@ export default Vue.extend({
|
||||
if (this.game.isStarted && !this.game.isEnded) {
|
||||
this.pollingClock = setInterval(() => {
|
||||
const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join(''));
|
||||
this.connection.send({
|
||||
type: 'check',
|
||||
crc32
|
||||
this.connection.send('check', {
|
||||
crc32: crc32
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
@ -224,9 +223,8 @@ export default Vue.extend({
|
||||
sound.play();
|
||||
}
|
||||
|
||||
this.connection.send({
|
||||
type: 'set',
|
||||
pos
|
||||
this.connection.send('set', {
|
||||
pos: pos
|
||||
});
|
||||
|
||||
this.checkEnd();
|
||||
|
@ -149,9 +149,9 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
created() {
|
||||
this.connection.on('change-accepts', this.onChangeAccepts);
|
||||
this.connection.on('update-settings', this.onUpdateSettings);
|
||||
this.connection.on('init-form', this.onInitForm);
|
||||
this.connection.on('changeAccepts', this.onChangeAccepts);
|
||||
this.connection.on('updateSettings', this.onUpdateSettings);
|
||||
this.connection.on('initForm', this.onInitForm);
|
||||
this.connection.on('message', this.onMessage);
|
||||
|
||||
if (this.game.user1Id != this.$store.state.i.id && this.game.settings.form1) this.form = this.game.settings.form1;
|
||||
@ -159,9 +159,9 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.off('change-accepts', this.onChangeAccepts);
|
||||
this.connection.off('update-settings', this.onUpdateSettings);
|
||||
this.connection.off('init-form', this.onInitForm);
|
||||
this.connection.off('changeAccepts', this.onChangeAccepts);
|
||||
this.connection.off('updateSettings', this.onUpdateSettings);
|
||||
this.connection.off('initForm', this.onInitForm);
|
||||
this.connection.off('message', this.onMessage);
|
||||
},
|
||||
|
||||
@ -171,15 +171,11 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
accept() {
|
||||
this.connection.send({
|
||||
type: 'accept'
|
||||
});
|
||||
this.connection.send('accept', {});
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.connection.send({
|
||||
type: 'cancel-accept'
|
||||
});
|
||||
this.connection.send('cancelAccept', {});
|
||||
},
|
||||
|
||||
onChangeAccepts(accepts) {
|
||||
@ -189,8 +185,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
updateSettings() {
|
||||
this.connection.send({
|
||||
type: 'update-settings',
|
||||
this.connection.send('updateSettings', {
|
||||
settings: this.game.settings
|
||||
});
|
||||
},
|
||||
@ -216,8 +211,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
onChangeForm(item) {
|
||||
this.connection.send({
|
||||
type: 'update-form',
|
||||
this.connection.send('updateForm', {
|
||||
id: item.id,
|
||||
value: item.value
|
||||
});
|
||||
@ -238,9 +232,9 @@ export default Vue.extend({
|
||||
const y = Math.floor(pos / this.game.settings.map[0].length);
|
||||
const newPixel =
|
||||
pixel == ' ' ? '-' :
|
||||
pixel == '-' ? 'b' :
|
||||
pixel == 'b' ? 'w' :
|
||||
' ';
|
||||
pixel == '-' ? 'b' :
|
||||
pixel == 'b' ? 'w' :
|
||||
' ';
|
||||
const line = this.game.settings.map[y].split('');
|
||||
line[x] = newPixel;
|
||||
this.$set(this.game.settings.map, y, line.join(''));
|
||||
|
@ -71,8 +71,7 @@ export default Vue.extend({
|
||||
|
||||
this.pingClock = setInterval(() => {
|
||||
if (this.matching) {
|
||||
this.connection.send({
|
||||
type: 'ping',
|
||||
this.connection.send('ping', {
|
||||
id: this.matching.id
|
||||
});
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.connection =((this as any).os.stream.connectToChannel('messaging', { otherparty: this.user.id });
|
||||
this.connection = (this as any).os.stream.connectToChannel('messaging', { otherparty: this.user.id });
|
||||
|
||||
this.connection.on('message', this.onMessage);
|
||||
this.connection.on('read', this.onRead);
|
||||
@ -174,8 +174,7 @@ export default Vue.extend({
|
||||
|
||||
this.messages.push(message);
|
||||
if (message.userId != this.$store.state.i.id && !document.hidden) {
|
||||
this.connection.send({
|
||||
type: 'read',
|
||||
this.connection.send('read', {
|
||||
id: message.id
|
||||
});
|
||||
}
|
||||
@ -247,8 +246,7 @@ export default Vue.extend({
|
||||
if (document.hidden) return;
|
||||
this.messages.forEach(message => {
|
||||
if (message.userId !== this.$store.state.i.id && !message.isRead) {
|
||||
this.connection.send({
|
||||
type: 'read',
|
||||
this.connection.send('read', {
|
||||
id: message.id
|
||||
});
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
import Vue from 'vue';
|
||||
import { url } from '../../../config';
|
||||
import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
|
||||
import Ok from './ok.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['note', 'source', 'compact'],
|
||||
@ -78,6 +79,7 @@ export default Vue.extend({
|
||||
(this as any).api('i/pin', {
|
||||
noteId: this.note.id
|
||||
}).then(() => {
|
||||
(this as any).os.new(Ok);
|
||||
this.destroyDom();
|
||||
});
|
||||
},
|
||||
@ -103,6 +105,7 @@ export default Vue.extend({
|
||||
(this as any).api('notes/favorites/create', {
|
||||
noteId: this.note.id
|
||||
}).then(() => {
|
||||
(this as any).os.new(Ok);
|
||||
this.destroyDom();
|
||||
});
|
||||
},
|
||||
|
175
src/client/app/common/views/components/ok.vue
Normal file
175
src/client/app/common/views/components/ok.vue
Normal file
@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div class="yvbkymdqeusiqucuuloahhiqflzinufs">
|
||||
<div class="bg" ref="bg"></div>
|
||||
<div class="body" ref="body">
|
||||
<div class="icon">
|
||||
<div class="circle left"></div>
|
||||
<span class="check tip"></span>
|
||||
<span class="check long"></span>
|
||||
<div class="ring"></div>
|
||||
<div class="fix"></div>
|
||||
<div class="circle right"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as anime from 'animejs';
|
||||
|
||||
export default Vue.extend({
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
anime({
|
||||
targets: this.$refs.bg,
|
||||
opacity: 1,
|
||||
duration: 300,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
||||
anime({
|
||||
targets: this.$refs.body,
|
||||
opacity: 1,
|
||||
scale: [1.2, 1],
|
||||
duration: 300,
|
||||
easing: [0, 0.5, 0.5, 1]
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
anime({
|
||||
targets: this.$refs.bg,
|
||||
opacity: 0,
|
||||
duration: 300,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
||||
anime({
|
||||
targets: this.$refs.body,
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
duration: 300,
|
||||
easing: [0.5, 0, 1, 0.5],
|
||||
complete: () => this.destroyDom()
|
||||
});
|
||||
}, 1250);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.yvbkymdqeusiqucuuloahhiqflzinufs
|
||||
pointer-events none
|
||||
|
||||
> .bg
|
||||
display block
|
||||
position fixed
|
||||
z-index 10000
|
||||
top 0
|
||||
left 0
|
||||
width 100%
|
||||
height 100%
|
||||
background rgba(#000, 0.7)
|
||||
opacity 0
|
||||
|
||||
> .body
|
||||
position fixed
|
||||
z-index 10000
|
||||
top 0
|
||||
right 0
|
||||
left 0
|
||||
bottom 0
|
||||
margin auto
|
||||
width 150px
|
||||
height 150px
|
||||
background var(--face)
|
||||
border-radius 8px
|
||||
opacity 0
|
||||
|
||||
> .icon
|
||||
display flex
|
||||
justify-content center
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
left 0
|
||||
bottom 0
|
||||
width 5em
|
||||
height 5em
|
||||
margin auto
|
||||
border .25em solid transparent
|
||||
border-radius 50%
|
||||
line-height 5em
|
||||
cursor default
|
||||
box-sizing content-box
|
||||
user-select none
|
||||
zoom normal
|
||||
border-color #a5dc86
|
||||
|
||||
> .circle
|
||||
position absolute
|
||||
width 3.75em
|
||||
height 7.5em
|
||||
transform rotate(45deg)
|
||||
border-radius 50%
|
||||
background var(--face)
|
||||
|
||||
&.left
|
||||
top -.4375em
|
||||
left -2.0635em
|
||||
transform rotate(-45deg)
|
||||
transform-origin 3.75em 3.75em
|
||||
border-radius 7.5em 0 0 7.5em
|
||||
|
||||
&.right
|
||||
top -.6875em
|
||||
left 1.875em
|
||||
transform rotate(-45deg)
|
||||
transform-origin 0 3.75em
|
||||
border-radius 0 7.5em 7.5em 0
|
||||
animation swal2-rotate-success-circular-line 4.25s ease-in
|
||||
|
||||
> .check
|
||||
display block
|
||||
position absolute
|
||||
height .3125em
|
||||
border-radius .125em
|
||||
background-color #a5dc86
|
||||
z-index 2
|
||||
|
||||
&.tip
|
||||
top 2.875em
|
||||
left .875em
|
||||
width 1.5625em
|
||||
transform rotate(45deg)
|
||||
animation swal2-animate-success-line-tip .75s
|
||||
|
||||
&.long
|
||||
top 2.375em
|
||||
right .5em
|
||||
width 2.9375em
|
||||
transform rotate(-45deg)
|
||||
animation swal2-animate-success-line-long .75s
|
||||
|
||||
> .fix
|
||||
position absolute
|
||||
top .5em
|
||||
left 1.625em
|
||||
width .4375em
|
||||
height 5.625em
|
||||
transform rotate(-45deg)
|
||||
z-index 1
|
||||
background var(--face)
|
||||
|
||||
> .ring
|
||||
position absolute
|
||||
top -.25em
|
||||
left -.25em
|
||||
width 100%
|
||||
height 100%
|
||||
border .25em solid rgba(165,220,134,.3)
|
||||
border-radius 50%
|
||||
z-index 2
|
||||
box-sizing content-box
|
||||
</style>
|
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="mk-reactions-viewer">
|
||||
<template v-if="reactions">
|
||||
<span :class="{notReacted}" @click="react('like')" v-if="reactions.like"><mk-reaction-icon reaction="like"/><span>{{ reactions.like }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('love')" v-if="reactions.love"><mk-reaction-icon reaction="love"/><span>{{ reactions.love }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('laugh')" v-if="reactions.laugh"><mk-reaction-icon reaction="laugh"/><span>{{ reactions.laugh }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('hmm')" v-if="reactions.hmm"><mk-reaction-icon reaction="hmm"/><span>{{ reactions.hmm }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('surprise')" v-if="reactions.surprise"><mk-reaction-icon reaction="surprise"/><span>{{ reactions.surprise }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('congrats')" v-if="reactions.congrats"><mk-reaction-icon reaction="congrats"/><span>{{ reactions.congrats }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('angry')" v-if="reactions.angry"><mk-reaction-icon reaction="angry"/><span>{{ reactions.angry }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('confused')" v-if="reactions.confused"><mk-reaction-icon reaction="confused"/><span>{{ reactions.confused }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('rip')" v-if="reactions.rip"><mk-reaction-icon reaction="rip"/><span>{{ reactions.rip }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('pudding')" v-if="reactions.pudding"><mk-reaction-icon reaction="pudding"/><span>{{ reactions.pudding }}</span></span>
|
||||
<span :class="{ reacted: note.myReaction == 'like' }" @click="react('like')" v-if="reactions.like"><mk-reaction-icon reaction="like"/><span>{{ reactions.like }}</span></span>
|
||||
<span :class="{ reacted: note.myReaction == 'love' }" @click="react('love')" v-if="reactions.love"><mk-reaction-icon reaction="love"/><span>{{ reactions.love }}</span></span>
|
||||
<span :class="{ reacted: note.myReaction == 'laugh' }" @click="react('laugh')" v-if="reactions.laugh"><mk-reaction-icon reaction="laugh"/><span>{{ reactions.laugh }}</span></span>
|
||||
<span :class="{ reacted: note.myReaction == 'hmm' }" @click="react('hmm')" v-if="reactions.hmm"><mk-reaction-icon reaction="hmm"/><span>{{ reactions.hmm }}</span></span>
|
||||
<span :class="{ reacted: note.myReaction == 'surprise' }" @click="react('surprise')" v-if="reactions.surprise"><mk-reaction-icon reaction="surprise"/><span>{{ reactions.surprise }}</span></span>
|
||||
<span :class="{ reacted: note.myReaction == 'congrats' }" @click="react('congrats')" v-if="reactions.congrats"><mk-reaction-icon reaction="congrats"/><span>{{ reactions.congrats }}</span></span>
|
||||
<span :class="{ reacted: note.myReaction == 'angry' }" @click="react('angry')" v-if="reactions.angry"><mk-reaction-icon reaction="angry"/><span>{{ reactions.angry }}</span></span>
|
||||
<span :class="{ reacted: note.myReaction == 'confused' }" @click="react('confused')" v-if="reactions.confused"><mk-reaction-icon reaction="confused"/><span>{{ reactions.confused }}</span></span>
|
||||
<span :class="{ reacted: note.myReaction == 'rip' }" @click="react('rip')" v-if="reactions.rip"><mk-reaction-icon reaction="rip"/><span>{{ reactions.rip }}</span></span>
|
||||
<span :class="{ reacted: note.myReaction == 'pudding' }" @click="react('pudding')" v-if="reactions.pudding"><mk-reaction-icon reaction="pudding"/><span>{{ reactions.pudding }}</span></span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@ -22,9 +22,6 @@ export default Vue.extend({
|
||||
computed: {
|
||||
reactions(): number {
|
||||
return this.note.reactionCounts;
|
||||
},
|
||||
notReacted(): boolean {
|
||||
return this.note.myReaction == null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -40,25 +37,42 @@ export default Vue.extend({
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-reactions-viewer
|
||||
border-top dashed 1px var(--reactionViewerBorder)
|
||||
border-bottom dashed 1px var(--reactionViewerBorder)
|
||||
margin 4px 0
|
||||
margin 6px 0
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> span
|
||||
margin-right 8px
|
||||
display inline-block
|
||||
height 32px
|
||||
margin-right 6px
|
||||
padding 0 6px
|
||||
border-radius 4px
|
||||
|
||||
&.notReacted
|
||||
*
|
||||
user-select none
|
||||
pointer-events none
|
||||
|
||||
&.reacted
|
||||
background var(--primary)
|
||||
|
||||
> span
|
||||
color var(--primaryForeground)
|
||||
|
||||
&:not(.reacted)
|
||||
cursor pointer
|
||||
background var(--reactionViewerButtonBg)
|
||||
|
||||
&:hover
|
||||
background var(--reactionViewerButtonHoverBg)
|
||||
|
||||
> .mk-reaction-icon
|
||||
font-size 1.4em
|
||||
|
||||
> span
|
||||
margin-left 4px
|
||||
font-size 1.2em
|
||||
font-size 1.1em
|
||||
line-height 32px
|
||||
vertical-align middle
|
||||
color var(--text)
|
||||
|
||||
</style>
|
||||
|
@ -67,22 +67,30 @@
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>%fa:folder-open% %i18n:@installed-themes%</summary>
|
||||
<ui-select v-model="selectedInstalledThemeId" placeholder="%i18n:@select-theme%">
|
||||
<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
<summary>%fa:folder-open% %i18n:@manage-themes%</summary>
|
||||
<ui-select v-model="selectedThemeId" placeholder="%i18n:@select-theme%">
|
||||
<optgroup label="%i18n:@builtin-themes%">
|
||||
<option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup label="%i18n:@my-themes%">
|
||||
<option v-for="x in installedThemes.filter(t => t.author == this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup label="%i18n:@installed-themes%">
|
||||
<option v-for="x in installedThemes.filter(t => t.author != this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</ui-select>
|
||||
<template v-if="selectedInstalledTheme">
|
||||
<ui-input readonly :value="selectedInstalledTheme.author">
|
||||
<template v-if="selectedTheme">
|
||||
<ui-input readonly :value="selectedTheme.author">
|
||||
<span>%i18n:@author%</span>
|
||||
</ui-input>
|
||||
<ui-textarea v-if="selectedInstalledTheme.desc" readonly :value="selectedInstalledTheme.desc">
|
||||
<ui-textarea v-if="selectedTheme.desc" readonly :value="selectedTheme.desc">
|
||||
<span>%i18n:@desc%</span>
|
||||
</ui-textarea>
|
||||
<ui-textarea readonly :value="selectedInstalledThemeCode">
|
||||
<ui-textarea readonly :value="selectedThemeCode">
|
||||
<span>%i18n:@theme-code%</span>
|
||||
</ui-textarea>
|
||||
<ui-button @click="export_()" link :download="`${selectedInstalledTheme.name}.misskeytheme`" ref="export">%fa:box% %i18n:@export%</ui-button>
|
||||
<ui-button @click="uninstall()">%fa:trash-alt R% %i18n:@uninstall%</ui-button>
|
||||
<ui-button @click="export_()" link :download="`${selectedTheme.name}.misskeytheme`" ref="export">%fa:box% %i18n:@export%</ui-button>
|
||||
<ui-button @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)">%fa:trash-alt R% %i18n:@uninstall%</ui-button>
|
||||
</template>
|
||||
</details>
|
||||
</div>
|
||||
@ -117,8 +125,9 @@ export default Vue.extend({
|
||||
|
||||
data() {
|
||||
return {
|
||||
builtinThemes: builtinThemes,
|
||||
installThemeCode: null,
|
||||
selectedInstalledThemeId: null,
|
||||
selectedThemeId: null,
|
||||
myThemeBase: 'light',
|
||||
myThemeName: '',
|
||||
myThemeDesc: '',
|
||||
@ -155,14 +164,14 @@ export default Vue.extend({
|
||||
set(value) { this.$store.commit('device/set', { key: 'darkTheme', value }); }
|
||||
},
|
||||
|
||||
selectedInstalledTheme() {
|
||||
if (this.selectedInstalledThemeId == null) return null;
|
||||
return this.installedThemes.find(x => x.id == this.selectedInstalledThemeId);
|
||||
selectedTheme() {
|
||||
if (this.selectedThemeId == null) return null;
|
||||
return this.themes.find(x => x.id == this.selectedThemeId);
|
||||
},
|
||||
|
||||
selectedInstalledThemeCode() {
|
||||
if (this.selectedInstalledTheme == null) return null;
|
||||
return JSON5.stringify(this.selectedInstalledTheme, null, '\t');
|
||||
selectedThemeCode() {
|
||||
if (this.selectedTheme == null) return null;
|
||||
return JSON5.stringify(this.selectedTheme, null, '\t');
|
||||
},
|
||||
|
||||
myTheme(): any {
|
||||
@ -210,7 +219,10 @@ export default Vue.extend({
|
||||
try {
|
||||
theme = JSON5.parse(code);
|
||||
} catch (e) {
|
||||
alert('%i18n:@invalid-theme%');
|
||||
this.$swal({
|
||||
type: 'error',
|
||||
text: '%i18n:@invalid-theme%'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -220,12 +232,18 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
if (theme.id == null) {
|
||||
alert('%i18n:@invalid-theme%');
|
||||
this.$swal({
|
||||
type: 'error',
|
||||
text: '%i18n:@invalid-theme%'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$store.state.device.themes.some(t => t.id == theme.id)) {
|
||||
alert('%i18n:@already-installed%');
|
||||
this.$swal({
|
||||
type: 'info',
|
||||
text: '%i18n:@already-installed%'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -234,16 +252,23 @@ export default Vue.extend({
|
||||
key: 'themes', value: themes
|
||||
});
|
||||
|
||||
alert('%i18n:@installed%'.replace('{}', theme.name));
|
||||
this.$swal({
|
||||
type: 'success',
|
||||
text: '%i18n:@installed%'.replace('{}', theme.name)
|
||||
});
|
||||
},
|
||||
|
||||
uninstall() {
|
||||
const theme = this.selectedInstalledTheme;
|
||||
const theme = this.selectedTheme;
|
||||
const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
|
||||
this.$store.commit('device/set', {
|
||||
key: 'themes', value: themes
|
||||
});
|
||||
alert('%i18n:@uninstalled%'.replace('{}', theme.name));
|
||||
|
||||
this.$swal({
|
||||
type: 'info',
|
||||
text: '%i18n:@uninstalled%'.replace('{}', theme.name)
|
||||
});
|
||||
},
|
||||
|
||||
import_() {
|
||||
@ -251,7 +276,7 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
export_() {
|
||||
const blob = new Blob([this.selectedInstalledThemeCode], {
|
||||
const blob = new Blob([this.selectedThemeCode], {
|
||||
type: 'application/json5'
|
||||
});
|
||||
this.$refs.export.$el.href = window.URL.createObjectURL(blob);
|
||||
@ -275,16 +300,26 @@ export default Vue.extend({
|
||||
|
||||
gen() {
|
||||
const theme = this.myTheme;
|
||||
|
||||
if (theme.name == null || theme.name.trim() == '') {
|
||||
alert('%i18n:@theme-name-required%');
|
||||
this.$swal({
|
||||
type: 'warning',
|
||||
text: '%i18n:@theme-name-required%'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
theme.id = uuid();
|
||||
|
||||
const themes = this.$store.state.device.themes.concat(theme);
|
||||
this.$store.commit('device/set', {
|
||||
key: 'themes', value: themes
|
||||
});
|
||||
alert('%i18n:@saved%');
|
||||
|
||||
this.$swal({
|
||||
type: 'success',
|
||||
text: '%i18n:@saved%'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -113,8 +113,7 @@ export default define({
|
||||
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
this.connection.send({
|
||||
type: 'requestLog',
|
||||
this.connection.send('requestLog',{
|
||||
id: Math.random().toString()
|
||||
});
|
||||
},
|
||||
|
@ -91,8 +91,7 @@ export default Vue.extend({
|
||||
mounted() {
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
this.connection.send({
|
||||
type: 'requestLog',
|
||||
this.connection.send('requestLog', {
|
||||
id: Math.random().toString()
|
||||
});
|
||||
},
|
||||
|
@ -8,7 +8,6 @@
|
||||
<router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link>
|
||||
<p class="username">@{{ user | acct }}</p>
|
||||
</div>
|
||||
<mk-follow-button :user="user"/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="empty" v-if="!fetching && users.length == 0">%i18n:@empty%</p>
|
||||
|
@ -181,8 +181,7 @@ export default Vue.extend({
|
||||
|
||||
onNotification(notification) {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.connection.send({
|
||||
type: 'readNotification',
|
||||
(this as any).os.stream.send('readNotification', {
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
|
@ -26,12 +26,14 @@ export default Vue.extend({
|
||||
this.init();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.connection.close();
|
||||
this.connection.dispose();
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
if (this.connection) this.connection.close();
|
||||
this.connection = new UserListStream((this as any).os, this.$store.state.i, this.list.id);
|
||||
if (this.connection) this.connection.dispose();
|
||||
this.connection = (this as any).os.stream.connectToChannel('userList', {
|
||||
listId: this.list.id
|
||||
});
|
||||
this.connection.on('note', this.onNote);
|
||||
this.connection.on('userAdded', this.onUserAdded);
|
||||
this.connection.on('userRemoved', this.onUserRemoved);
|
||||
|
@ -77,8 +77,7 @@ export default Vue.extend({
|
||||
mounted() {
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
this.connection.send({
|
||||
type: 'requestLog',
|
||||
this.connection.send('requestLog', {
|
||||
id: Math.random().toString(),
|
||||
length: 200
|
||||
});
|
||||
|
@ -46,8 +46,10 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.connection) this.connection.close();
|
||||
this.connection = new UserListStream((this as any).os, this.$store.state.i, this.list.id);
|
||||
if (this.connection) this.connection.dispose();
|
||||
this.connection = (this as any).os.stream.connectToChannel('userList', {
|
||||
listId: this.list.id
|
||||
});
|
||||
this.connection.on('note', this.onNote);
|
||||
this.connection.on('userAdded', this.onUserAdded);
|
||||
this.connection.on('userRemoved', this.onUserRemoved);
|
||||
@ -56,7 +58,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.close();
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -113,8 +113,7 @@ export default Vue.extend({
|
||||
|
||||
onNotification(notification) {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.connection.send({
|
||||
type: 'readNotification',
|
||||
(this as any).os.stream.send('readNotification', {
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
|
@ -13,7 +13,6 @@
|
||||
<router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link>
|
||||
<p class="username">@{{ _user | acct }}</p>
|
||||
</div>
|
||||
<mk-follow-button :user="_user"/>
|
||||
</div>
|
||||
</template>
|
||||
<p class="empty" v-else>%i18n:@no-one%</p>
|
||||
|
@ -8,6 +8,7 @@ import VueRouter from 'vue-router';
|
||||
import * as TreeView from 'vue-json-tree-view';
|
||||
import VAnimateCss from 'v-animate-css';
|
||||
import VModal from 'vue-js-modal';
|
||||
import VueSweetalert2 from 'vue-sweetalert2';
|
||||
|
||||
import VueHotkey from './common/hotkey';
|
||||
import App from './app.vue';
|
||||
@ -26,6 +27,7 @@ Vue.use(TreeView);
|
||||
Vue.use(VAnimateCss);
|
||||
Vue.use(VModal);
|
||||
Vue.use(VueHotkey);
|
||||
Vue.use(VueSweetalert2);
|
||||
|
||||
// Register global directives
|
||||
require('./common/views/directives');
|
||||
|
@ -212,7 +212,7 @@ export default class MiOS extends EventEmitter {
|
||||
const fetched = () => {
|
||||
this.emit('signedin');
|
||||
|
||||
this.stream = new Stream(this);
|
||||
this.initStream();
|
||||
|
||||
// Finish init
|
||||
callback();
|
||||
@ -247,12 +247,103 @@ export default class MiOS extends EventEmitter {
|
||||
// Finish init
|
||||
callback();
|
||||
|
||||
this.stream = new Stream(this);
|
||||
this.initStream();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
private initStream() {
|
||||
this.stream = new Stream(this);
|
||||
|
||||
if (this.store.getters.isSignedIn) {
|
||||
const main = this.stream.useSharedConnection('main');
|
||||
|
||||
// 自分の情報が更新されたとき
|
||||
main.on('meUpdated', i => {
|
||||
this.store.dispatch('mergeMe', i);
|
||||
});
|
||||
|
||||
main.on('readAllNotifications', () => {
|
||||
this.store.dispatch('mergeMe', {
|
||||
hasUnreadNotification: false
|
||||
});
|
||||
});
|
||||
|
||||
main.on('unreadNotification', () => {
|
||||
this.store.dispatch('mergeMe', {
|
||||
hasUnreadNotification: true
|
||||
});
|
||||
});
|
||||
|
||||
main.on('readAllMessagingMessages', () => {
|
||||
this.store.dispatch('mergeMe', {
|
||||
hasUnreadMessagingMessage: false
|
||||
});
|
||||
});
|
||||
|
||||
main.on('unreadMessagingMessage', () => {
|
||||
this.store.dispatch('mergeMe', {
|
||||
hasUnreadMessagingMessage: true
|
||||
});
|
||||
});
|
||||
|
||||
main.on('unreadMention', () => {
|
||||
this.store.dispatch('mergeMe', {
|
||||
hasUnreadMentions: true
|
||||
});
|
||||
});
|
||||
|
||||
main.on('readAllUnreadMentions', () => {
|
||||
this.store.dispatch('mergeMe', {
|
||||
hasUnreadMentions: false
|
||||
});
|
||||
});
|
||||
|
||||
main.on('unreadSpecifiedNote', () => {
|
||||
this.store.dispatch('mergeMe', {
|
||||
hasUnreadSpecifiedNotes: true
|
||||
});
|
||||
});
|
||||
|
||||
main.on('readAllUnreadSpecifiedNotes', () => {
|
||||
this.store.dispatch('mergeMe', {
|
||||
hasUnreadSpecifiedNotes: false
|
||||
});
|
||||
});
|
||||
|
||||
main.on('clientSettingUpdated', x => {
|
||||
this.store.commit('settings/set', {
|
||||
key: x.key,
|
||||
value: x.value
|
||||
});
|
||||
});
|
||||
|
||||
main.on('homeUpdated', x => {
|
||||
this.store.commit('settings/setHome', x);
|
||||
});
|
||||
|
||||
main.on('mobileHomeUpdated', x => {
|
||||
this.store.commit('settings/setMobileHome', x);
|
||||
});
|
||||
|
||||
main.on('widgetUpdated', x => {
|
||||
this.store.commit('settings/setWidget', {
|
||||
id: x.id,
|
||||
data: x.data
|
||||
});
|
||||
});
|
||||
|
||||
// トークンが再生成されたとき
|
||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||
main.on('myTokenRegenerated', () => {
|
||||
alert('%i18n:common.my-token-regenerated%');
|
||||
this.signout();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register service worker
|
||||
*/
|
||||
|
@ -91,8 +91,6 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
|
||||
.mk-dialog
|
||||
> .bg
|
||||
display block
|
||||
|
@ -77,6 +77,8 @@ export default Vue.extend({
|
||||
|
||||
methods: {
|
||||
fetchMoreNotifications() {
|
||||
if (this.fetchingMoreNotifications) return;
|
||||
|
||||
this.fetchingMoreNotifications = true;
|
||||
|
||||
const max = 30;
|
||||
@ -98,8 +100,7 @@ export default Vue.extend({
|
||||
|
||||
onNotification(notification) {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.connection.send({
|
||||
type: 'readNotification',
|
||||
(this as any).os.stream.send('readNotification', {
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
|
@ -58,8 +58,7 @@ export default Vue.extend({
|
||||
methods: {
|
||||
onNotification(notification) {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.connection.send({
|
||||
type: 'readNotification',
|
||||
(this as any).os.stream.send('readNotification', {
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
|
@ -36,13 +36,15 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.close();
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
if (this.connection) this.connection.close();
|
||||
this.connection = new UserListStream((this as any).os, this.$store.state.i, this.list.id);
|
||||
if (this.connection) this.connection.dispose();
|
||||
this.connection = (this as any).os.stream.connectToChannel('userList', {
|
||||
listId: this.list.id
|
||||
});
|
||||
this.connection.on('note', this.onNote);
|
||||
this.connection.on('userAdded', this.onUserAdded);
|
||||
this.connection.on('userRemoved', this.onUserRemoved);
|
||||
|
@ -170,7 +170,10 @@ export default Vue.extend({
|
||||
this.$store.state.i.bannerUrl = i.bannerUrl;
|
||||
|
||||
if (notify) {
|
||||
alert('%i18n:@saved%');
|
||||
this.$swal({
|
||||
type: 'success',
|
||||
text: '%i18n:@saved%'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -26,7 +26,7 @@
|
||||
face: '$secondary',
|
||||
faceText: '#fff',
|
||||
faceHeader: ':lighten<5<$secondary',
|
||||
faceHeaderText: '#e3e5e8',
|
||||
faceHeaderText: '$text',
|
||||
faceDivider: 'rgba(0, 0, 0, 0.3)',
|
||||
faceTextButton: '$text',
|
||||
faceTextButtonHover: ':lighten<10<$text',
|
||||
@ -84,7 +84,8 @@
|
||||
|
||||
reactionPickerButtonHoverBg: 'rgba(255, 255, 255, 0.18)',
|
||||
|
||||
reactionViewerBorder: 'rgba(255, 255, 255, 0.1)',
|
||||
reactionViewerButtonBg: 'rgba(255, 255, 255, 0.1)',
|
||||
reactionViewerButtonHoverBg: 'rgba(255, 255, 255, 0.2)',
|
||||
|
||||
pollEditorInputBg: 'rgba(0, 0, 0, 0.25)',
|
||||
|
||||
|
@ -84,7 +84,8 @@
|
||||
|
||||
reactionPickerButtonHoverBg: '#eee',
|
||||
|
||||
reactionViewerBorder: 'rgba(0, 0, 0, 0.1)',
|
||||
reactionViewerButtonBg: 'rgba(0, 0, 0, 0.05)',
|
||||
reactionViewerButtonHoverBg: 'rgba(0, 0, 0, 0.1)',
|
||||
|
||||
pollEditorInputBg: '#fff',
|
||||
|
||||
|
@ -93,9 +93,13 @@ export type Source = {
|
||||
private_key: string;
|
||||
};
|
||||
|
||||
google_maps_api_key: string;
|
||||
|
||||
clusterLimit?: number;
|
||||
|
||||
user_recommendation?: {
|
||||
external: boolean;
|
||||
engine: string;
|
||||
timeout: number;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1,10 +1,10 @@
|
||||
import * as redis from 'redis';
|
||||
import config from '../config';
|
||||
|
||||
export default redis.createClient(
|
||||
export default config.redis ? redis.createClient(
|
||||
config.redis.port,
|
||||
config.redis.host,
|
||||
{
|
||||
auth_pass: config.redis.pass
|
||||
}
|
||||
);
|
||||
) : null;
|
||||
|
@ -111,6 +111,7 @@ async function workerMain() {
|
||||
*/
|
||||
async function init(): Promise<Config> {
|
||||
Logger.info('Welcome to Misskey!');
|
||||
Logger.info(`<<< Misskey v${pkg.version} >>>`);
|
||||
|
||||
new Logger('Deps').info(`Node.js ${process.version}`);
|
||||
MachineInfo.show();
|
||||
|
@ -17,7 +17,7 @@ const summarize = (note: any): string => {
|
||||
summary += note.text ? note.text : '';
|
||||
|
||||
// ファイルが添付されているとき
|
||||
if (note.files.length != 0) {
|
||||
if ((note.files || []).length != 0) {
|
||||
summary += ` (${note.files.length}つのファイル)`;
|
||||
}
|
||||
|
||||
|
@ -166,7 +166,7 @@ export const pack = (
|
||||
|
||||
// (データベースの欠損などで)ファイルがデータベース上に見つからなかったとき
|
||||
if (_file == null) {
|
||||
console.warn(`in packaging driveFile: driveFile not found on database: ${_file}`);
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: driveFile :: ${file}`);
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@ export const pack = (
|
||||
|
||||
// (データベースの不具合などで)投稿が見つからなかったら
|
||||
if (_favorite.note == null) {
|
||||
console.warn(`in packaging favorite: note not found on database: ${_favorite.noteId}`);
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: favorite -> note :: ${_favorite.id} (note ${_favorite.noteId})`);
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
|
@ -281,9 +281,9 @@ export const pack = async (
|
||||
_note = deepcopy(note);
|
||||
}
|
||||
|
||||
// 投稿がデータベース上に見つからなかったとき
|
||||
// (データベースの欠損などで)投稿がデータベース上に見つからなかったとき
|
||||
if (_note == null) {
|
||||
console.warn(`note not found on database: ${note}`);
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: note :: ${note}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -380,12 +380,25 @@ export const pack = async (
|
||||
// resolve promises in _note object
|
||||
_note = await rap(_note);
|
||||
|
||||
// (データベースの欠損などで)ユーザーがデータベース上に見つからなかったとき
|
||||
//#region (データベースの欠損などで)参照しているデータがデータベース上に見つからなかったとき
|
||||
if (_note.user == null) {
|
||||
console.warn(`in packaging note: note user not found on database: note(${_note.id})`);
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: note -> user :: ${_note.id} (user ${_note.userId})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (opts.detail) {
|
||||
if (_note.replyId != null && _note.reply == null) {
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: note -> reply :: ${_note.id} (reply ${_note.replyId})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_note.renoteId != null && _note.renote == null) {
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: note -> renote :: ${_note.id} (renote ${_note.renoteId})`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
if (_note.user.isCat && _note.text) {
|
||||
_note.text = _note.text.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ');
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ export const pack = (notification: any) => new Promise<any>(async (resolve, reje
|
||||
|
||||
// (データベースの不具合などで)投稿が見つからなかったら
|
||||
if (_notification.note == null) {
|
||||
console.warn(`in packaging notification: note not found on database: ${_notification.noteId}`);
|
||||
console.warn(`[DAMAGED DB] (missing) pkg: notification -> note :: ${_notification.id} (note ${_notification.noteId})`);
|
||||
return resolve(null);
|
||||
}
|
||||
break;
|
||||
|
@ -24,15 +24,15 @@ export const meta = {
|
||||
},
|
||||
};
|
||||
|
||||
export default (params: any) => new Promise(async (res, rej) => {
|
||||
export default async (params: any) => {
|
||||
const [ps, psErr] = getParams(meta, params);
|
||||
if (psErr) return rej(psErr);
|
||||
if (psErr) throw psErr;
|
||||
|
||||
const object = await fetchAny(ps.uri);
|
||||
if (object !== null) return res(object);
|
||||
if (object !== null) return object;
|
||||
|
||||
return rej('object not found');
|
||||
});
|
||||
throw new Error('object not found');
|
||||
};
|
||||
|
||||
/***
|
||||
* URIからUserかNoteを解決する
|
||||
|
@ -27,7 +27,8 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
|
||||
|
||||
const file = await DriveFile.findOne({
|
||||
md5: md5,
|
||||
'metadata.userId': user._id
|
||||
'metadata.userId': user._id,
|
||||
'metadata.deletedAt': { $exists: false }
|
||||
});
|
||||
|
||||
if (file === null) {
|
||||
|
@ -22,7 +22,8 @@ export default async (params: any, user: ILocalUser) => {
|
||||
const file = await DriveFile
|
||||
.findOne({
|
||||
_id: fileId,
|
||||
'metadata.userId': user._id
|
||||
'metadata.userId': user._id,
|
||||
'metadata.deletedAt': { $exists: false }
|
||||
});
|
||||
|
||||
if (file === null) {
|
||||
|
@ -1,6 +1,3 @@
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import * as os from 'os';
|
||||
import config from '../../../config';
|
||||
import Meta from '../../../models/meta';
|
||||
@ -9,9 +6,17 @@ import { ILocalUser } from '../../../models/user';
|
||||
const pkg = require('../../../../package.json');
|
||||
const client = require('../../../../built/client/meta.json');
|
||||
|
||||
/**
|
||||
* Show core info
|
||||
*/
|
||||
export const meta = {
|
||||
desc: {
|
||||
'ja-JP': 'インスタンス情報を取得します。',
|
||||
'en-US': 'Get the information of this instance.'
|
||||
},
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
params: {},
|
||||
};
|
||||
|
||||
export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
|
||||
const meta: any = (await Meta.findOne()) || {};
|
||||
|
||||
@ -28,10 +33,12 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
|
||||
machine: os.hostname(),
|
||||
os: os.platform(),
|
||||
node: process.version,
|
||||
|
||||
cpu: {
|
||||
model: os.cpus()[0].model,
|
||||
cores: os.cpus().length
|
||||
},
|
||||
|
||||
broadcasts: meta.broadcasts || [],
|
||||
disableRegistration: meta.disableRegistration,
|
||||
disableLocalTimeline: meta.disableLocalTimeline,
|
||||
@ -40,6 +47,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
|
||||
swPublickey: config.sw ? config.sw.public_key : null,
|
||||
hidedTags: (me && me.isAdmin) ? meta.hidedTags : undefined,
|
||||
bannerUrl: meta.bannerUrl,
|
||||
|
||||
features: {
|
||||
registration: !meta.disableRegistration,
|
||||
localTimeLine: !meta.disableLocalTimeline,
|
||||
|
@ -3,6 +3,8 @@ import $ from 'cafy';
|
||||
import User, { pack, ILocalUser } from '../../../../models/user';
|
||||
import { getFriendIds } from '../../common/get-friends';
|
||||
import Mute from '../../../../models/mute';
|
||||
import * as request from 'request';
|
||||
import config from '../../../../config';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
@ -15,44 +17,75 @@ export const meta = {
|
||||
};
|
||||
|
||||
export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
|
||||
// Get 'limit' parameter
|
||||
const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
|
||||
if (limitErr) return rej('invalid limit param');
|
||||
if (config.user_recommendation && config.user_recommendation.external) {
|
||||
const userName = me.username;
|
||||
const hostName = config.hostname;
|
||||
const limit = params.limit;
|
||||
const offset = params.offset;
|
||||
const timeout = config.user_recommendation.timeout;
|
||||
const engine = config.user_recommendation.engine;
|
||||
const url = engine
|
||||
.replace('{{host}}', hostName)
|
||||
.replace('{{user}}', userName)
|
||||
.replace('{{limit}}', limit)
|
||||
.replace('{{offset}}', offset);
|
||||
|
||||
// Get 'offset' parameter
|
||||
const [offset = 0, offsetErr] = $.num.optional.min(0).get(params.offset);
|
||||
if (offsetErr) return rej('invalid offset param');
|
||||
|
||||
// ID list of the user itself and other users who the user follows
|
||||
const followingIds = await getFriendIds(me._id);
|
||||
|
||||
// ミュートしているユーザーを取得
|
||||
const mutedUserIds = (await Mute.find({
|
||||
muterId: me._id
|
||||
})).map(m => m.muteeId);
|
||||
|
||||
const users = await User
|
||||
.find({
|
||||
_id: {
|
||||
$nin: followingIds.concat(mutedUserIds)
|
||||
request(
|
||||
{
|
||||
url: url,
|
||||
timeout: timeout,
|
||||
json: true,
|
||||
followRedirect: true,
|
||||
followAllRedirects: true
|
||||
},
|
||||
isLocked: false,
|
||||
$or: [{
|
||||
lastUsedAt: {
|
||||
$gte: new Date(Date.now() - ms('7days'))
|
||||
(error: any, response: any, body: any) => {
|
||||
if (!error && response.statusCode == 200) {
|
||||
res(body);
|
||||
} else {
|
||||
res([]);
|
||||
}
|
||||
}, {
|
||||
host: null
|
||||
}]
|
||||
}, {
|
||||
limit: limit,
|
||||
skip: offset,
|
||||
sort: {
|
||||
followersCount: -1
|
||||
}
|
||||
});
|
||||
);
|
||||
} else {
|
||||
// Get 'limit' parameter
|
||||
const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
|
||||
if (limitErr) return rej('invalid limit param');
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(users.map(async user =>
|
||||
await pack(user, me, { detail: true }))));
|
||||
// Get 'offset' parameter
|
||||
const [offset = 0, offsetErr] = $.num.optional.min(0).get(params.offset);
|
||||
if (offsetErr) return rej('invalid offset param');
|
||||
|
||||
// ID list of the user itself and other users who the user follows
|
||||
const followingIds = await getFriendIds(me._id);
|
||||
|
||||
// ミュートしているユーザーを取得
|
||||
const mutedUserIds = (await Mute.find({
|
||||
muterId: me._id
|
||||
})).map(m => m.muteeId);
|
||||
|
||||
const users = await User
|
||||
.find({
|
||||
_id: {
|
||||
$nin: followingIds.concat(mutedUserIds)
|
||||
},
|
||||
isLocked: { $ne: true },
|
||||
$or: [{
|
||||
lastUsedAt: {
|
||||
$gte: new Date(Date.now() - ms('7days'))
|
||||
}
|
||||
}, {
|
||||
host: null
|
||||
}]
|
||||
}, {
|
||||
limit: limit,
|
||||
skip: offset,
|
||||
sort: {
|
||||
followersCount: -1
|
||||
}
|
||||
});
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(users.map(async user =>
|
||||
await pack(user, me, { detail: true }))));
|
||||
}
|
||||
});
|
||||
|
@ -8,6 +8,12 @@ import { IUser } from '../../models/user';
|
||||
const log = debug('misskey:limitter');
|
||||
|
||||
export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) => {
|
||||
// Redisがインストールされてない場合は常に許可
|
||||
if (limiterDB == null) {
|
||||
ok();
|
||||
return;
|
||||
}
|
||||
|
||||
const limitation = endpoint.meta.limit;
|
||||
|
||||
const key = limitation.hasOwnProperty('key')
|
||||
|
@ -55,7 +55,7 @@ router.get('/disconnect/twitter', async ctx => {
|
||||
}));
|
||||
});
|
||||
|
||||
if (config.twitter == null) {
|
||||
if (config.twitter == null || redis == null) {
|
||||
router.get('/connect/twitter', ctx => {
|
||||
ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)';
|
||||
});
|
||||
|
@ -7,6 +7,8 @@ import Connection from '.';
|
||||
export default abstract class Channel {
|
||||
protected connection: Connection;
|
||||
public id: string;
|
||||
public abstract readonly chName: string;
|
||||
public static readonly shouldShare: boolean;
|
||||
|
||||
protected get user() {
|
||||
return this.connection.user;
|
||||
|
@ -2,6 +2,9 @@ import autobind from 'autobind-decorator';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'drive';
|
||||
public static shouldShare = true;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
// Subscribe drive stream
|
||||
|
@ -1,5 +1,6 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import * as CRC32 from 'crc-32';
|
||||
import * as mongo from 'mongodb';
|
||||
import ReversiGame, { pack } from '../../../../../models/games/reversi/game';
|
||||
import { publishReversiGameStream } from '../../../../../stream';
|
||||
import Reversi from '../../../../../games/reversi/core';
|
||||
@ -7,11 +8,14 @@ import * as maps from '../../../../../games/reversi/maps';
|
||||
import Channel from '../../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
private gameId: string;
|
||||
public readonly chName = 'gamesReversiGame';
|
||||
public static shouldShare = false;
|
||||
|
||||
private gameId: mongo.ObjectID;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
this.gameId = params.gameId as string;
|
||||
this.gameId = new mongo.ObjectID(params.gameId as string);
|
||||
|
||||
// Subscribe game stream
|
||||
this.subscriber.on(`reversiGameStream:${this.gameId}`, data => {
|
||||
@ -23,10 +27,10 @@ export default class extends Channel {
|
||||
public onMessage(type: string, body: any) {
|
||||
switch (type) {
|
||||
case 'accept': this.accept(true); break;
|
||||
case 'cancel-accept': this.accept(false); break;
|
||||
case 'update-settings': this.updateSettings(body.settings); break;
|
||||
case 'init-form': this.initForm(body); break;
|
||||
case 'update-form': this.updateForm(body.id, body.value); break;
|
||||
case 'cancelAccept': this.accept(false); break;
|
||||
case 'updateSettings': this.updateSettings(body.settings); break;
|
||||
case 'initForm': this.initForm(body); break;
|
||||
case 'updateForm': this.updateForm(body.id, body.value); break;
|
||||
case 'message': this.message(body); break;
|
||||
case 'set': this.set(body.pos); break;
|
||||
case 'check': this.check(body.crc32); break;
|
||||
|
@ -5,6 +5,9 @@ import { publishMainStream } from '../../../../../stream';
|
||||
import Channel from '../../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'gamesReversi';
|
||||
public static shouldShare = true;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
// Subscribe reversi stream
|
||||
|
@ -5,6 +5,9 @@ import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'globalTimeline';
|
||||
public static shouldShare = true;
|
||||
|
||||
private mutedUserIds: string[] = [];
|
||||
|
||||
@autobind
|
||||
|
@ -5,6 +5,9 @@ import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'hashtag';
|
||||
public static shouldShare = false;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null;
|
||||
|
@ -5,6 +5,9 @@ import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'homeTimeline';
|
||||
public static shouldShare = true;
|
||||
|
||||
private mutedUserIds: string[] = [];
|
||||
|
||||
@autobind
|
||||
|
@ -5,6 +5,9 @@ import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'hybridTimeline';
|
||||
public static shouldShare = true;
|
||||
|
||||
private mutedUserIds: string[] = [];
|
||||
|
||||
@autobind
|
||||
|
@ -5,6 +5,9 @@ import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'localTimeline';
|
||||
public static shouldShare = true;
|
||||
|
||||
private mutedUserIds: string[] = [];
|
||||
|
||||
@autobind
|
||||
|
@ -3,6 +3,9 @@ import Mute from '../../../../models/mute';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'main';
|
||||
public static shouldShare = true;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
const mute = await Mute.find({ muterId: this.user._id });
|
||||
|
@ -2,6 +2,9 @@ import autobind from 'autobind-decorator';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'messagingIndex';
|
||||
public static shouldShare = true;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
// Subscribe messaging index stream
|
||||
|
@ -3,6 +3,9 @@ import read from '../../common/read-messaging-message';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'messaging';
|
||||
public static shouldShare = false;
|
||||
|
||||
private otherpartyId: string;
|
||||
|
||||
@autobind
|
||||
|
@ -5,6 +5,9 @@ import Channel from '../channel';
|
||||
const ev = new Xev();
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'notesStats';
|
||||
public static shouldShare = true;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
ev.addListener('notesStats', this.onStats);
|
||||
|
@ -5,6 +5,9 @@ import Channel from '../channel';
|
||||
const ev = new Xev();
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'serverStats';
|
||||
public static shouldShare = true;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
ev.addListener('serverStats', this.onStats);
|
||||
|
@ -2,6 +2,9 @@ import autobind from 'autobind-decorator';
|
||||
import Channel from '../channel';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'userList';
|
||||
public static shouldShare = false;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
const listId = params.listId as string;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import * as websocket from 'websocket';
|
||||
import Xev from 'xev';
|
||||
import * as debug from 'debug';
|
||||
|
||||
import User, { IUser } from '../../../models/user';
|
||||
@ -11,6 +10,7 @@ import readNote from '../../../services/note/read';
|
||||
|
||||
import Channel from './channel';
|
||||
import channels from './channels';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
const log = debug('misskey');
|
||||
|
||||
@ -21,14 +21,14 @@ export default class Connection {
|
||||
public user?: IUser;
|
||||
public app: IApp;
|
||||
private wsConnection: websocket.connection;
|
||||
public subscriber: Xev;
|
||||
public subscriber: EventEmitter;
|
||||
private channels: Channel[] = [];
|
||||
private subscribingNotes: any = {};
|
||||
public sendMessageToWsOverride: any = null; // 後方互換性のため
|
||||
|
||||
constructor(
|
||||
wsConnection: websocket.connection,
|
||||
subscriber: Xev,
|
||||
subscriber: EventEmitter,
|
||||
user: IUser,
|
||||
app: IApp
|
||||
) {
|
||||
@ -58,6 +58,7 @@ export default class Connection {
|
||||
case 'connect': this.onChannelConnectRequested(body); break;
|
||||
case 'disconnect': this.onChannelDisconnectRequested(body); break;
|
||||
case 'channel': this.onChannelMessageRequested(body); break;
|
||||
case 'ch': this.onChannelMessageRequested(body); break; // alias
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,7 +148,7 @@ export default class Connection {
|
||||
private onChannelConnectRequested(payload: any) {
|
||||
const { channel, id, params } = payload;
|
||||
log(`CH CONNECT: ${id} ${channel} by @${this.user.username}`);
|
||||
this.connectChannel(id, params, (channels as any)[channel]);
|
||||
this.connectChannel(id, params, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -176,10 +177,18 @@ export default class Connection {
|
||||
* チャンネルに接続
|
||||
*/
|
||||
@autobind
|
||||
public connectChannel(id: string, params: any, channelClass: { new(id: string, connection: Connection): Channel }) {
|
||||
const channel = new channelClass(id, this);
|
||||
this.channels.push(channel);
|
||||
channel.init(params);
|
||||
public connectChannel(id: string, params: any, channel: string) {
|
||||
// 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視
|
||||
if ((channels as any)[channel].shouldShare && this.channels.some(c => c.chName === channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ch: Channel = new (channels as any)[channel](id, this);
|
||||
this.channels.push(ch);
|
||||
ch.init(params);
|
||||
this.sendMessageToWs('connected', {
|
||||
id: id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,11 +1,13 @@
|
||||
import * as http from 'http';
|
||||
import * as websocket from 'websocket';
|
||||
import * as redis from 'redis';
|
||||
import Xev from 'xev';
|
||||
|
||||
import MainStreamConnection from './stream';
|
||||
import { ParsedUrlQuery } from 'querystring';
|
||||
import authenticate from './authenticate';
|
||||
import channels from './stream/channels';
|
||||
import { EventEmitter } from 'events';
|
||||
import config from '../../config';
|
||||
|
||||
module.exports = (server: http.Server) => {
|
||||
// Init websocket server
|
||||
@ -16,11 +18,34 @@ module.exports = (server: http.Server) => {
|
||||
ws.on('request', async (request) => {
|
||||
const connection = request.accept();
|
||||
|
||||
const ev = new Xev();
|
||||
|
||||
const q = request.resourceURL.query as ParsedUrlQuery;
|
||||
const [user, app] = await authenticate(q.i as string);
|
||||
|
||||
let ev: EventEmitter;
|
||||
|
||||
if (config.redis) {
|
||||
// Connect to Redis
|
||||
const subscriber = redis.createClient(
|
||||
config.redis.port, config.redis.host);
|
||||
|
||||
subscriber.subscribe('misskey');
|
||||
|
||||
ev = new EventEmitter();
|
||||
|
||||
subscriber.on('message', async (_, data) => {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
ev.emit(obj.channel, obj.message);
|
||||
});
|
||||
|
||||
connection.once('close', () => {
|
||||
subscriber.unsubscribe();
|
||||
subscriber.quit();
|
||||
});
|
||||
} else {
|
||||
ev = new Xev();
|
||||
}
|
||||
|
||||
const main = new MainStreamConnection(connection, ev, user, app);
|
||||
|
||||
// 後方互換性のため
|
||||
@ -40,10 +65,14 @@ module.exports = (server: http.Server) => {
|
||||
};
|
||||
|
||||
main.connectChannel(Math.random().toString(), null,
|
||||
request.resourceURL.pathname === '/' ? channels.homeTimeline :
|
||||
request.resourceURL.pathname === '/local-timeline' ? channels.localTimeline :
|
||||
request.resourceURL.pathname === '/hybrid-timeline' ? channels.hybridTimeline :
|
||||
request.resourceURL.pathname === '/global-timeline' ? channels.globalTimeline : null);
|
||||
request.resourceURL.pathname === '/' ? 'homeTimeline' :
|
||||
request.resourceURL.pathname === '/local-timeline' ? 'localTimeline' :
|
||||
request.resourceURL.pathname === '/hybrid-timeline' ? 'hybridTimeline' :
|
||||
request.resourceURL.pathname === '/global-timeline' ? 'globalTimeline' : null);
|
||||
|
||||
if (request.resourceURL.pathname === '/') {
|
||||
main.connectChannel(Math.random().toString(), null, 'main');
|
||||
}
|
||||
}
|
||||
|
||||
connection.once('close', () => {
|
||||
|
@ -44,7 +44,8 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise
|
||||
});
|
||||
|
||||
publishNoteStream(note._id, 'reacted', {
|
||||
reaction: reaction
|
||||
reaction: reaction,
|
||||
userId: user._id
|
||||
});
|
||||
|
||||
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as mongo from 'mongodb';
|
||||
import redis from './db/redis';
|
||||
import Xev from 'xev';
|
||||
import Meta, { IMeta } from './models/meta';
|
||||
|
||||
@ -9,7 +10,10 @@ class Publisher {
|
||||
private meta: IMeta;
|
||||
|
||||
constructor() {
|
||||
this.ev = new Xev();
|
||||
// Redisがインストールされてないときはプロセス間通信を使う
|
||||
if (redis == null) {
|
||||
this.ev = new Xev();
|
||||
}
|
||||
|
||||
setInterval(async () => {
|
||||
this.meta = await Meta.findOne({});
|
||||
@ -28,7 +32,14 @@ class Publisher {
|
||||
{ type: type, body: null } :
|
||||
{ type: type, body: value };
|
||||
|
||||
this.ev.emit(channel, message);
|
||||
if (this.ev) {
|
||||
this.ev.emit(channel, message);
|
||||
} else {
|
||||
redis.publish('misskey', JSON.stringify({
|
||||
channel: channel,
|
||||
message: message
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public publishMainStream = (userId: ID, type: string, value?: any): void => {
|
||||
|
83
src/tools/move-drive-files.ts
Normal file
83
src/tools/move-drive-files.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import * as Minio from 'minio';
|
||||
import * as uuid from 'uuid';
|
||||
import * as promiseLimit from 'promise-limit';
|
||||
import DriveFile, { DriveFileChunk, getDriveFileBucket, IDriveFile } from '../models/drive-file';
|
||||
import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../models/drive-file-thumbnail';
|
||||
import config from '../config';
|
||||
|
||||
const limit = promiseLimit(16);
|
||||
|
||||
DriveFile.find({
|
||||
$or: [{
|
||||
'metadata.withoutChunks': { $exists: false }
|
||||
}, {
|
||||
'metadata.withoutChunks': false
|
||||
}],
|
||||
'metadata.deletedAt': { $exists: false }
|
||||
}, {
|
||||
fields: {
|
||||
_id: true
|
||||
}
|
||||
}).then(async files => {
|
||||
console.log(`there is ${files.length} files`);
|
||||
|
||||
await Promise.all(files.map(file => limit(() => job(file))));
|
||||
|
||||
console.log('ALL DONE');
|
||||
});
|
||||
|
||||
async function job(file: IDriveFile): Promise<any> {
|
||||
file = await DriveFile.findOne({ _id: file._id });
|
||||
|
||||
const minio = new Minio.Client(config.drive.config);
|
||||
|
||||
const name = file.filename;
|
||||
const keyDir = `${config.drive.prefix}/${uuid.v4()}`;
|
||||
const key = `${keyDir}/${name}`;
|
||||
const thumbnailKeyDir = `${config.drive.prefix}/${uuid.v4()}`;
|
||||
const thumbnailKey = `${thumbnailKeyDir}/${name}.thumbnail.jpg`;
|
||||
|
||||
const baseUrl = config.drive.baseUrl
|
||||
|| `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`;
|
||||
|
||||
const bucket = await getDriveFileBucket();
|
||||
const readable = bucket.openDownloadStream(file._id);
|
||||
|
||||
await minio.putObject(config.drive.bucket, key, readable, file.length, {
|
||||
'Content-Type': file.contentType,
|
||||
'Cache-Control': 'max-age=31536000, immutable'
|
||||
});
|
||||
|
||||
await DriveFile.findOneAndUpdate({ _id: file._id }, {
|
||||
$set: {
|
||||
'metadata.withoutChunks': true,
|
||||
'metadata.storage': 'minio',
|
||||
'metadata.storageProps': {
|
||||
key: key,
|
||||
thumbnailKey: thumbnailKey
|
||||
},
|
||||
'metadata.url': `${ baseUrl }/${ keyDir }/${ encodeURIComponent(name) }`,
|
||||
}
|
||||
});
|
||||
|
||||
// チャンクをすべて削除
|
||||
await DriveFileChunk.remove({
|
||||
files_id: file._id
|
||||
});
|
||||
|
||||
//#region サムネイルもあれば削除
|
||||
const thumbnail = await DriveFileThumbnail.findOne({
|
||||
'metadata.originalId': file._id
|
||||
});
|
||||
|
||||
if (thumbnail) {
|
||||
await DriveFileThumbnailChunk.remove({
|
||||
files_id: thumbnail._id
|
||||
});
|
||||
|
||||
await DriveFileThumbnail.remove({ _id: thumbnail._id });
|
||||
}
|
||||
//#endregion
|
||||
|
||||
console.log('done', file._id);
|
||||
}
|
Reference in New Issue
Block a user