Compare commits

...

44 Commits
9.0.0 ... 9.7.0

Author SHA1 Message Date
11b84a04b3 9.7.0 2018-10-05 01:59:26 +09:00
f243ce66e7 ActivityPubのHTTPリクエストの強化 (#2820)
* Fix error handling in AP deliver

* Set timeout to resolver

* Tune looks
2018-10-05 01:58:41 +09:00
baf9b65801 Improve error handling of packaging functions 2018-10-05 01:43:47 +09:00
401d0b1298 9.5.0 2018-10-04 13:55:25 +09:00
fce7dc0f4e Improve error handling of pack function of notification 2018-10-04 13:53:48 +09:00
35489ef5b7 Improve default theme definition (dark) 2018-10-04 13:48:26 +09:00
546d494587 Improve default theme definition (light) 2018-10-04 13:40:13 +09:00
e8afa2c940 Improve pack function of favorite 2018-10-04 13:33:59 +09:00
c1ef1bf605 fix(package): update typescript-eslint-parser to version 19.0.2 (#2819)
Closes #2772
2018-10-04 12:53:50 +09:00
4ab0dbe7e3 fix(package): update @types/webpack to version 4.4.14 (#2818)
Closes #2778
2018-10-04 12:53:17 +09:00
44f86a94f4 fix(package): update @types/node to version 10.11.4 (#2817)
Closes #2765
2018-10-04 12:52:57 +09:00
a0278154a3 fix(package): update webpack-cli to version 3.1.2 (#2816)
Closes #2757
2018-10-04 12:52:27 +09:00
8b7e6b200e fix(package): update jsdom to version 12.1.0 (#2788) 2018-10-04 12:51:19 +09:00
d6f6c26725 fix(package): update @types/qrcode to version 1.3.0 (#2813) 2018-10-04 12:50:32 +09:00
cf66343b31 fix(package): update qrcode to version 1.3.0 (#2799) 2018-10-04 12:50:25 +09:00
d53689332f fix(package): update diskusage to version 0.2.5 (#2767) 2018-10-04 12:49:56 +09:00
4105237027 fix(package): update koa-mount to version 4.0.0 (#2776) 2018-10-04 12:49:38 +09:00
436962e4b8 fix(package): update nan to version 2.11.1 (#2784) 2018-10-04 12:49:05 +09:00
a85efa1392 fix(package): update gulp-htmlmin to version 5.0.1 (#2815)
Closes #2669
2018-10-04 12:48:52 +09:00
f0115a5e21 fix(package): update webpack to version 4.20.2 (#2814)
Closes #2768
2018-10-04 12:45:47 +09:00
80105239dc 9.4.0 2018-10-04 00:39:48 +09:00
baad11288a ドキュメントが見つからなくてもエラーにせずnullを返すように 2018-10-04 00:39:11 +09:00
7e50646ede 9.3.1 2018-10-03 23:01:58 +09:00
d4b8e47bcb Fix 2018-10-03 23:01:47 +09:00
0eefd2922c 9.3.0 2018-10-03 22:55:08 +09:00
30c0f98691 Fix bug 2018-10-03 22:54:10 +09:00
06a7c2e138 Revert "Enable JSON5 syntax"
This reverts commit 6e04549a9b.
2018-10-03 22:14:05 +09:00
3537b3de8e Clean up 2018-10-03 22:05:17 +09:00
ed6450244d Feature flags feature (#2803) 2018-10-03 12:39:03 +09:00
e813880392 6個ピン留めできてしまうの修正 (#2804) 2018-10-03 12:37:23 +09:00
9a57efa6d9 ✌️ 2018-10-03 03:13:10 +09:00
31de530497 9.2.0 2018-10-03 03:09:04 +09:00
16b6b1f2b3 テーマ関連機能の強化 2018-10-03 03:07:46 +09:00
a2a25eb5f8 Improve usability 2018-10-03 03:00:55 +09:00
274cf1af1c テーマ関連の機能を強化 2018-10-03 02:57:31 +09:00
7d11c8b767 Improve admin UI (#2802) 2018-10-02 23:42:46 +09:00
7e50e03cfb Update README.md [AUTOGEN] (#2801) 2018-10-02 20:23:27 +09:00
89d5df20a5 9.1.0 2018-10-02 16:29:05 +09:00
c09a2a37fe リモートのピン留め投稿取得対応 (#2798)
* Fetch featured

* Handle featured change

* Fix typo
2018-10-02 16:27:36 +09:00
b5745877ca 🎨 2018-10-02 16:23:55 +09:00
6e04549a9b Enable JSON5 syntax 2018-10-02 16:13:14 +09:00
38139ee6c9 テーマに関して強化 2018-10-02 16:10:45 +09:00
6b96bd0185 テーマに関して強化 2018-10-02 16:04:31 +09:00
f2b9863eea Better deployment option descriptions. (#2800) 2018-10-02 11:59:12 +09:00
66 changed files with 1152 additions and 848 deletions

View File

@ -7,27 +7,51 @@ maintainer:
repository_url: https://github.com/syuilo/misskey # Repository URL repository_url: https://github.com/syuilo/misskey # Repository URL
feedback_url: https://github.com/syuilo/misskey/issues # Feedback URL (e.g. github issue) feedback_url: https://github.com/syuilo/misskey/issues # Feedback URL (e.g. github issue)
# URL and Port settings overview
# e.g., If you want to realize following structure:
#
# +--- https://example.com:123 ----------+
# +------+ |+-------------+ +---------------+|
# | User | ---> || Proxy (123) | ---> | Misskey (456) ||
# +------+ |+-------------+ +---------------+|
# +--------------------------------------+
#
# You need to set 'https://example.com:123' to 'url' prop and
# You need to set 456 to 'port' prop.
#
# In other words, the 'url' prop should be the final accessible URL seen by a user.
# 'port' prop is a port that the Misskey server should actually listen
# on and it is not necessarily the port that a user accesses.
url: http://localhost/ # Final accessible URL seen by a user.
url: https://example.tld/
### Port and TLS settings ######################################
#
# Misskey supports two deployment options for public.
#
# Option 1: With Reverse Proxy
#
# +----- https://example.tld/ ------------+
# +------+ |+-------------+ +----------------+|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
# +------+ |+-------------+ +----------------+|
# +---------------------------------------+
#
# You need to setup reverse proxy. (eg. Nginx)
# You do not define 'https' section.
# Option 2: Standalone
#
# +- https://example.tld/ -+
# +------+ | +---------------+ |
# | User | ---> | | Misskey (443) | |
# +------+ | +---------------+ |
# +------------------------+
#
# You need to run Misskey as root.
# You need to set Certificate in 'https' section.
# To use option 1, uncomment below line.
# port: 3000 # A port that your Misskey server should listen.
# To use option 2, uncomment below lines.
# port: 443
#
# https:
# # path for certification
# key: /etc/letsencrypt/live/example.tld/privkey.pem
# cert: /etc/letsencrypt/live/example.tld/fullchain.pem
################################################################
# A port that your Misskey server should listen.
# This value is not a port to use when accessing with a browser.
port: 80
mongodb: mongodb:
host: localhost host: localhost
@ -98,12 +122,6 @@ drive:
# Below settings are optional # Below settings are optional
# #
# TLS
# https:
# # path for certification
# key: /etc/letsencrypt/live/example.tld/privkey.pem
# cert: /etc/letsencrypt/live/example.tld/fullchain.pem
# Elasticsearch # Elasticsearch
# elasticsearch: # elasticsearch:
# host: localhost # host: localhost

View File

@ -73,17 +73,17 @@ Please see [Contribution guide](./CONTRIBUTING.md).
<table><tr> <table><tr>
<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/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/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/1?token-time=2145916800&token-hash=f03BFb4S2FUx9YEt87TnEmifb4h33OywGBW2akQVtQY%3D" alt="Melilot"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/2?token-time=2145916800&token-hash=mgPdX9TqZxEg4TTPuc477dxhIgYk9246qafjWZEqZ7g%3D" alt="Melilot"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12999811/5f349fafcce44dd1824a8b1ebbec4564/2?token-time=2145916800&token-hash=rwZ8qvbm_kpA4ib3kc07tVKupXeySpY5ATQFGxfL9v0%3D" alt="Axella"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12999811/5f349fafcce44dd1824a8b1ebbec4564/2?token-time=2145916800&token-hash=rwZ8qvbm_kpA4ib3kc07tVKupXeySpY5ATQFGxfL9v0%3D" alt="Xeltica"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/3384329/8b713330cb27404ea6e9fac50ff96efe/1?token-time=2145916800&token-hash=0eu4-m1gTWA9PhptVZt6rdKcusqcD7RB87rJT23VVFI%3D" alt="べすれい"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/3384329/8b713330cb27404ea6e9fac50ff96efe/1?token-time=2145916800&token-hash=0eu4-m1gTWA9PhptVZt6rdKcusqcD7RB87rJT23VVFI%3D" alt="べすれい"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=GgJ_NmUB6_nnRNLVGUWjV-WX91On7BOu59LKncYV9fE%3D" alt="gutfuckllc"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=GgJ_NmUB6_nnRNLVGUWjV-WX91On7BOu59LKncYV9fE%3D" alt="gutfuckllc"></td>
<td><img src="https://c8.patreon.com/2/100/12718187" alt="Peter G."></td> <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> <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> </tr><tr>
<td><a href="https://www.patreon.com/user?u=12731202">negao</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=13099460">ne_moni</a></td>
<td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td> <td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td>
<td><a href="https://www.patreon.com/AxellaMC">Axella</a></td> <td><a href="https://www.patreon.com/AxellaMC">Xeltica</a></td>
<td><a href="https://www.patreon.com/user?u=3384329">べすれい</a></td> <td><a href="https://www.patreon.com/user?u=3384329">べすれい</a></td>
<td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td> <td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td>
<td><a href="https://www.patreon.com/user?u=12718187">Peter G.</a></td> <td><a href="https://www.patreon.com/user?u=12718187">Peter G.</a></td>
@ -91,19 +91,17 @@ Please see [Contribution guide](./CONTRIBUTING.md).
</tr></table> </tr></table>
<table><tr> <table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/2?token-time=2145916800&token-hash=zElv7ZcPL3viGsXbNG_KWiKrbV0vvw1gk0panx8DJoo%3D" alt="Naoki Kosaka"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/2?token-time=2145916800&token-hash=zElv7ZcPL3viGsXbNG_KWiKrbV0vvw1gk0panx8DJoo%3D" alt="Naoki Kosaka"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12931605/ead494101f364dffa90efe49e36fb494/1?token-time=2145916800&token-hash=NzSFPjIlodXyv41rwK61aZWVZWfI4surJaNj8vWKvqM%3D" alt="Reiju"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1?token-time=2145916800&token-hash=UERBN4OyP7Nh5XwwdDg0N0IE5cD6_qUQMO81Z5Wizso%3D" alt="Hiratake"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1?token-time=2145916800&token-hash=UERBN4OyP7Nh5XwwdDg0N0IE5cD6_qUQMO81Z5Wizso%3D" alt="Hiratake"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=S1zP0QyLU52Dqq6dtc9qNYyWfW86XrYHiR4NMbeOrnA%3D" alt="dansup"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=S1zP0QyLU52Dqq6dtc9qNYyWfW86XrYHiR4NMbeOrnA%3D" alt="dansup"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D" alt="Takashi Shibuya"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D" alt="Takashi Shibuya"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/user?u=5881381">Naoki Kosaka</a></td> <td><a href="https://www.patreon.com/user?u=5881381">Naoki Kosaka</a></td>
<td><a href="https://www.patreon.com/user?u=12931605">Reiju</a></td>
<td><a href="https://www.patreon.com/hiratake">Hiratake</a></td> <td><a href="https://www.patreon.com/hiratake">Hiratake</a></td>
<td><a href="https://www.patreon.com/dansup">dansup</a></td> <td><a href="https://www.patreon.com/dansup">dansup</a></td>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td> <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table> </tr></table>
**Last updated:** Sun, 02 Sep 2018 05:30:06 UTC **Last updated:** Tue, 02 Oct 2018 09:25:07 UTC
<!-- PATREON_END --> <!-- PATREON_END -->
:four_leaf_clover: Copyright :four_leaf_clover: Copyright

View File

@ -2,7 +2,6 @@
* Gulp tasks * Gulp tasks
*/ */
import * as fs from 'fs';
import * as gulp from 'gulp'; import * as gulp from 'gulp';
import * as gutil from 'gulp-util'; import * as gutil from 'gulp-util';
import * as ts from 'gulp-typescript'; import * as ts from 'gulp-typescript';
@ -166,9 +165,7 @@ gulp.task('build:client:pug', [
.pipe(pug({ .pipe(pug({
locals: { locals: {
themeColor: constants.themeColor, themeColor: constants.themeColor,
facss: fa.dom.css(), facss: fa.dom.css()
//hljscss: fs.readFileSync('./node_modules/highlight.js/styles/default.css', 'utf8')
hljscss: fs.readFileSync('./src/client/assets/code-highlight.css', 'utf8')
} }
})) }))
.pipe(htmlmin({ .pipe(htmlmin({

View File

@ -309,6 +309,12 @@ common/views/components/theme.vue:
select-theme: "テーマを選択してください" select-theme: "テーマを選択してください"
uninstall: "アンインストール" uninstall: "アンインストール"
uninstalled: "「{}」をアンインストールしました" uninstalled: "「{}」をアンインストールしました"
author: "作者"
desc: "説明"
export: "エクスポート"
import: "インポート"
import-by-code: "またはコードをペースト"
theme-name-required: "テーマ名は必須です。"
common/views/components/cw-button.vue: common/views/components/cw-button.vue:
hide: "隠す" hide: "隠す"

View File

@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "9.0.0", "version": "9.7.0",
"clientVersion": "1.0.10049", "clientVersion": "1.0.10090",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,
@ -60,10 +60,10 @@
"@types/mocha": "5.2.3", "@types/mocha": "5.2.3",
"@types/mongodb": "3.1.7", "@types/mongodb": "3.1.7",
"@types/ms": "0.7.30", "@types/ms": "0.7.30",
"@types/node": "10.10.3", "@types/node": "10.11.4",
"@types/portscanner": "2.1.0", "@types/portscanner": "2.1.0",
"@types/pug": "2.0.4", "@types/pug": "2.0.4",
"@types/qrcode": "1.2.0", "@types/qrcode": "1.3.0",
"@types/ratelimiter": "2.1.28", "@types/ratelimiter": "2.1.28",
"@types/redis": "2.8.6", "@types/redis": "2.8.6",
"@types/request": "2.47.1", "@types/request": "2.47.1",
@ -78,7 +78,7 @@
"@types/tinycolor2": "1.4.1", "@types/tinycolor2": "1.4.1",
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",
"@types/uuid": "3.4.4", "@types/uuid": "3.4.4",
"@types/webpack": "4.4.12", "@types/webpack": "4.4.14",
"@types/webpack-stream": "3.2.10", "@types/webpack-stream": "3.2.10",
"@types/websocket": "0.0.40", "@types/websocket": "0.0.40",
"@types/ws": "6.0.1", "@types/ws": "6.0.1",
@ -98,7 +98,7 @@
"debug": "4.0.1", "debug": "4.0.1",
"deep-equal": "1.0.1", "deep-equal": "1.0.1",
"deepcopy": "0.6.3", "deepcopy": "0.6.3",
"diskusage": "0.2.4", "diskusage": "0.2.5",
"dompurify": "1.0.5", "dompurify": "1.0.5",
"double-ended-queue": "2.1.0-0", "double-ended-queue": "2.1.0-0",
"elasticsearch": "15.1.1", "elasticsearch": "15.1.1",
@ -113,7 +113,7 @@
"fuckadblock": "3.2.1", "fuckadblock": "3.2.1",
"gulp": "3.9.1", "gulp": "3.9.1",
"gulp-cssnano": "2.1.3", "gulp-cssnano": "2.1.3",
"gulp-htmlmin": "4.0.0", "gulp-htmlmin": "5.0.1",
"gulp-imagemin": "4.1.0", "gulp-imagemin": "4.1.0",
"gulp-mocha": "6.0.0", "gulp-mocha": "6.0.0",
"gulp-pug": "4.0.1", "gulp-pug": "4.0.1",
@ -133,14 +133,16 @@
"is-root": "2.0.0", "is-root": "2.0.0",
"is-url": "1.2.4", "is-url": "1.2.4",
"js-yaml": "3.12.0", "js-yaml": "3.12.0",
"jsdom": "11.12.0", "jsdom": "12.1.0",
"json5": "2.1.0",
"json5-loader": "1.0.1",
"koa": "2.5.1", "koa": "2.5.1",
"koa-bodyparser": "4.2.1", "koa-bodyparser": "4.2.1",
"koa-compress": "3.0.0", "koa-compress": "3.0.0",
"koa-favicon": "2.0.1", "koa-favicon": "2.0.1",
"koa-json-body": "5.3.0", "koa-json-body": "5.3.0",
"koa-logger": "3.2.0", "koa-logger": "3.2.0",
"koa-mount": "3.0.0", "koa-mount": "4.0.0",
"koa-multer": "1.0.2", "koa-multer": "1.0.2",
"koa-router": "7.4.0", "koa-router": "7.4.0",
"koa-send": "5.0.0", "koa-send": "5.0.0",
@ -157,7 +159,7 @@
"mongodb": "3.1.1", "mongodb": "3.1.1",
"monk": "6.0.6", "monk": "6.0.6",
"ms": "2.1.1", "ms": "2.1.1",
"nan": "2.11.0", "nan": "2.11.1",
"nested-property": "0.0.7", "nested-property": "0.0.7",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"object-assign-deep": "0.4.0", "object-assign-deep": "0.4.0",
@ -169,7 +171,7 @@
"promise-sequential": "1.1.1", "promise-sequential": "1.1.1",
"pug": "2.0.3", "pug": "2.0.3",
"punycode": "2.1.1", "punycode": "2.1.1",
"qrcode": "1.2.2", "qrcode": "1.3.0",
"ratelimiter": "3.2.0", "ratelimiter": "3.2.0",
"recaptcha-promise": "0.1.3", "recaptcha-promise": "0.1.3",
"reconnecting-websocket": "3.2.2", "reconnecting-websocket": "3.2.2",
@ -201,7 +203,7 @@
"ts-node": "7.0.1", "ts-node": "7.0.1",
"tslint": "5.10.0", "tslint": "5.10.0",
"typescript": "2.9.2", "typescript": "2.9.2",
"typescript-eslint-parser": "18.0.0", "typescript-eslint-parser": "19.0.2",
"uglify-es": "3.3.9", "uglify-es": "3.3.9",
"url-loader": "1.1.1", "url-loader": "1.1.1",
"uuid": "3.3.2", "uuid": "3.3.2",
@ -223,8 +225,8 @@
"vuex-persistedstate": "2.5.4", "vuex-persistedstate": "2.5.4",
"web-push": "3.3.3", "web-push": "3.3.3",
"webfinger.js": "2.6.6", "webfinger.js": "2.6.6",
"webpack": "4.19.1", "webpack-cli": "3.1.2",
"webpack-cli": "3.1.0", "webpack": "4.20.2",
"websocket": "1.0.28", "websocket": "1.0.28",
"ws": "6.0.0", "ws": "6.0.0",
"xev": "2.0.1" "xev": "2.0.1"

View File

@ -5,9 +5,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { url, lang } from './config'; import { url, lang } from './config';
import applyTheme from './common/scripts/theme';
const darkTheme = require('../theme/dark');
const halloweenTheme = require('../theme/halloween');
export default Vue.extend({ export default Vue.extend({
computed: { computed: {

View File

@ -34,9 +34,6 @@ html
//- FontAwesome style //- FontAwesome style
style #{facss} style #{facss}
//- highlight.js style
style #{hljscss}
body body
noscript: p noscript: p
| JavaScriptを有効にしてください | JavaScriptを有効にしてください

View File

@ -24,7 +24,6 @@
const theme = localStorage.getItem('theme'); const theme = localStorage.getItem('theme');
if (theme) { if (theme) {
Object.entries(JSON.parse(theme)).forEach(([k, v]) => { Object.entries(JSON.parse(theme)).forEach(([k, v]) => {
if (k == 'meta') return;
document.documentElement.style.setProperty(`--${k}`, v.toString()); document.documentElement.style.setProperty(`--${k}`, v.toString());
}); });
} }

View File

@ -3,19 +3,19 @@
<label> <label>
<span>%i18n:@light-theme%</span> <span>%i18n:@light-theme%</span>
<ui-select v-model="light" placeholder="%i18n:@light-theme%"> <ui-select v-model="light" placeholder="%i18n:@light-theme%">
<option v-for="x in themes" :value="x.meta.id" :key="x.meta.id">{{ x.meta.name }}</option> <option v-for="x in themes" :value="x.id" :key="x.id">{{ x.name }}</option>
</ui-select> </ui-select>
</label> </label>
<label> <label>
<span>%i18n:@dark-theme%</span> <span>%i18n:@dark-theme%</span>
<ui-select v-model="dark" placeholder="%i18n:@dark-theme%"> <ui-select v-model="dark" placeholder="%i18n:@dark-theme%">
<option v-for="x in themes" :value="x.meta.id" :key="x.meta.id">{{ x.meta.name }}</option> <option v-for="x in themes" :value="x.id" :key="x.id">{{ x.name }}</option>
</ui-select> </ui-select>
</label> </label>
<details class="creator"> <details class="creator">
<summary>%i18n:@create-a-theme%</summary> <summary>%fa:palette% %i18n:@create-a-theme%</summary>
<div> <div>
<span>%i18n:@base-theme%:</span> <span>%i18n:@base-theme%:</span>
<ui-radio v-model="myThemeBase" value="light">%i18n:@base-theme-light%</ui-radio> <ui-radio v-model="myThemeBase" value="light">%i18n:@base-theme-light%</ui-radio>
@ -25,6 +25,9 @@
<ui-input v-model="myThemeName"> <ui-input v-model="myThemeName">
<span>%i18n:@theme-name%</span> <span>%i18n:@theme-name%</span>
</ui-input> </ui-input>
<ui-textarea v-model="myThemeDesc">
<span>%i18n:@desc%</span>
</ui-textarea>
</div> </div>
<div> <div>
<div style="padding-bottom:8px;">%i18n:@primary-color%:</div> <div style="padding-bottom:8px;">%i18n:@primary-color%:</div>
@ -38,37 +41,64 @@
<div style="padding-bottom:8px;">%i18n:@text-color%:</div> <div style="padding-bottom:8px;">%i18n:@text-color%:</div>
<color-picker v-model="myThemeText"/> <color-picker v-model="myThemeText"/>
</div> </div>
<ui-button @click="preview()">%i18n:@preview-created-theme%</ui-button> <ui-button @click="preview()">%fa:eye% %i18n:@preview-created-theme%</ui-button>
<ui-button primary @click="gen()">%i18n:@save-created-theme%</ui-button> <ui-button primary @click="gen()">%fa:save R% %i18n:@save-created-theme%</ui-button>
</details> </details>
<details> <details>
<summary>%i18n:@install-a-theme%</summary> <summary>%fa:download% %i18n:@install-a-theme%</summary>
<ui-button @click="import_()">%fa:file-import% %i18n:@import%</ui-button>
<input ref="file" type="file" accept=".misskeytheme" style="display:none;" @change="onUpdateImportFile"/>
<p>%i18n:@import-by-code%:</p>
<ui-textarea v-model="installThemeCode"> <ui-textarea v-model="installThemeCode">
<span>%i18n:@theme-code%</span> <span>%i18n:@theme-code%</span>
</ui-textarea> </ui-textarea>
<ui-button @click="install()">%i18n:@install%</ui-button> <ui-button @click="() => install(this.installThemeCode)">%fa:check% %i18n:@install%</ui-button>
</details> </details>
<details> <details>
<summary>%i18n:@installed-themes%</summary> <summary>%fa:folder-open% %i18n:@installed-themes%</summary>
<ui-select v-model="selectedInstalledTheme" placeholder="%i18n:@select-theme%"> <ui-select v-model="selectedInstalledThemeId" placeholder="%i18n:@select-theme%">
<option v-for="x in installedThemes" :value="x.meta.id" :key="x.meta.id">{{ x.meta.name }}</option> <option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</ui-select> </ui-select>
<ui-textarea readonly :value="selectedInstalledThemeCode"> <template v-if="selectedInstalledTheme">
<span>%i18n:@theme-code%</span> <ui-input readonly :value="selectedInstalledTheme.author">
</ui-textarea> <span>%i18n:@author%</span>
<ui-button @click="uninstall()">%i18n:@uninstall%</ui-button> </ui-input>
<ui-textarea v-if="selectedInstalledTheme.desc" readonly :value="selectedInstalledTheme.desc">
<span>%i18n:@desc%</span>
</ui-textarea>
<ui-textarea readonly :value="selectedInstalledThemeCode">
<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>
</template>
</details> </details>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { lightTheme, darkTheme, builtinThemes, applyTheme } from '../../../theme'; import { lightTheme, darkTheme, builtinThemes, applyTheme, Theme } from '../../../theme';
import { Chrome } from 'vue-color'; import { Chrome } from 'vue-color';
import * as uuid from 'uuid'; import * as uuid from 'uuid';
import * as tinycolor from 'tinycolor2'; import * as tinycolor from 'tinycolor2';
import * as JSON5 from 'json5';
// 後方互換性のため
function convertOldThemedefinition(t) {
const t2 = {
id: t.meta.id,
name: t.meta.name,
author: t.meta.author,
base: t.meta.base,
vars: t.meta.vars,
props: t
};
delete t2.props.meta;
return t2;
}
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -78,21 +108,22 @@ export default Vue.extend({
data() { data() {
return { return {
installThemeCode: null, installThemeCode: null,
selectedInstalledTheme: null, selectedInstalledThemeId: null,
myThemeBase: 'light', myThemeBase: 'light',
myThemeName: '', myThemeName: '',
myThemePrimary: lightTheme.meta.vars.primary, myThemeDesc: '',
myThemeSecondary: lightTheme.meta.vars.secondary, myThemePrimary: lightTheme.vars.primary,
myThemeText: lightTheme.meta.vars.text myThemeSecondary: lightTheme.vars.secondary,
myThemeText: lightTheme.vars.text
}; };
}, },
computed: { computed: {
themes(): any { themes(): Theme[] {
return this.$store.state.device.themes.concat(builtinThemes); return this.$store.state.device.themes.concat(builtinThemes);
}, },
installedThemes(): any { installedThemes(): Theme[] {
return this.$store.state.device.themes; return this.$store.state.device.themes;
}, },
@ -106,22 +137,26 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'darkTheme', value }); } 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);
},
selectedInstalledThemeCode() { selectedInstalledThemeCode() {
if (this.selectedInstalledTheme == null) return null; if (this.selectedInstalledTheme == null) return null;
return JSON.stringify(this.installedThemes.find(x => x.meta.id == this.selectedInstalledTheme)); return JSON5.stringify(this.selectedInstalledTheme, null, '\t');
}, },
myTheme(): any { myTheme(): any {
return { return {
meta: { name: this.myThemeName,
name: this.myThemeName, author: this.$store.state.i.username,
author: this.$store.state.i.name, desc: this.myThemeDesc,
base: this.myThemeBase, base: this.myThemeBase,
vars: { vars: {
primary: tinycolor(typeof this.myThemePrimary == 'string' ? this.myThemePrimary : this.myThemePrimary.rgba).toRgbString(), primary: tinycolor(typeof this.myThemePrimary == 'string' ? this.myThemePrimary : this.myThemePrimary.rgba).toRgbString(),
secondary: tinycolor(typeof this.myThemeSecondary == 'string' ? this.myThemeSecondary : this.myThemeSecondary.rgba).toRgbString(), secondary: tinycolor(typeof this.myThemeSecondary == 'string' ? this.myThemeSecondary : this.myThemeSecondary.rgba).toRgbString(),
text: tinycolor(typeof this.myThemeText == 'string' ? this.myThemeText : this.myThemeText.rgba).toRgbString() text: tinycolor(typeof this.myThemeText == 'string' ? this.myThemeText : this.myThemeText.rgba).toRgbString()
}
} }
}; };
} }
@ -130,37 +165,90 @@ export default Vue.extend({
watch: { watch: {
myThemeBase(v) { myThemeBase(v) {
const theme = v == 'light' ? lightTheme : darkTheme; const theme = v == 'light' ? lightTheme : darkTheme;
this.myThemePrimary = theme.meta.vars.primary; this.myThemePrimary = theme.vars.primary;
this.myThemeSecondary = theme.meta.vars.secondary; this.myThemeSecondary = theme.vars.secondary;
this.myThemeText = theme.meta.vars.text; this.myThemeText = theme.vars.text;
} }
}, },
beforeCreate() {
// migrate old theme definitions
// 後方互換性のため
this.$store.commit('device/set', {
key: 'themes', value: this.$store.state.device.themes.map(t => {
if (t.id == null) {
return convertOldThemedefinition(t);
} else {
return t;
}
})
});
},
methods: { methods: {
install() { install(code) {
const theme = JSON.parse(this.installThemeCode); let theme;
if (theme.meta == null || theme.meta.id == null) {
try {
theme = JSON5.parse(code);
} catch (e) {
alert('%i18n:@invalid-theme%'); alert('%i18n:@invalid-theme%');
return; return;
} }
if (this.$store.state.device.themes.some(t => t.meta.id == theme.meta.id)) {
// 後方互換性のため
if (theme.id == null && theme.meta != null) {
theme = convertOldThemedefinition(theme);
}
if (theme.id == null) {
alert('%i18n:@invalid-theme%');
return;
}
if (this.$store.state.device.themes.some(t => t.id == theme.id)) {
alert('%i18n:@already-installed%'); alert('%i18n:@already-installed%');
return; return;
} }
const themes = this.$store.state.device.themes.concat(theme); const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', { this.$store.commit('device/set', {
key: 'themes', value: themes key: 'themes', value: themes
}); });
alert('%i18n:@installed%'.replace('{}', theme.meta.name));
alert('%i18n:@installed%'.replace('{}', theme.name));
}, },
uninstall() { uninstall() {
const theme = this.installedThemes.find(x => x.meta.id == this.selectedInstalledTheme); const theme = this.selectedInstalledTheme;
const themes = this.$store.state.device.themes.filter(t => t.meta.id != theme.meta.id); const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
this.$store.commit('device/set', { this.$store.commit('device/set', {
key: 'themes', value: themes key: 'themes', value: themes
}); });
alert('%i18n:@uninstalled%'.replace('{}', theme.meta.name)); alert('%i18n:@uninstalled%'.replace('{}', theme.name));
},
import_() {
(this.$refs.file as any).click();
}
export_() {
const blob = new Blob([this.selectedInstalledThemeCode], {
type: 'application/json5'
});
this.$refs.export.$el.href = window.URL.createObjectURL(blob);
},
onUpdateImportFile() {
const f = (this.$refs.file as any).files[0];
const reader = new FileReader();
reader.onload = e => {
this.install(e.target.result);
};
reader.readAsText(f);
}, },
preview() { preview() {
@ -169,7 +257,11 @@ export default Vue.extend({
gen() { gen() {
const theme = this.myTheme; const theme = this.myTheme;
theme.meta.id = uuid(); if (theme.name == null || theme.name.trim() == '') {
alert('%i18n:@theme-name-required%');
return;
}
theme.id = uuid();
const themes = this.$store.state.device.themes.concat(theme); const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', { this.$store.commit('device/set', {
key: 'themes', value: themes key: 'themes', value: themes
@ -182,6 +274,15 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
.nicnklzforebnpfgasiypmpdaaglujqm .nicnklzforebnpfgasiypmpdaaglujqm
> details
border-top solid 1px var(--faceDivider)
> summary
padding 16px 0
> *:last-child
margin-bottom 16px
> .creator > .creator
> div > div
padding 16px 0 padding 16px 0

View File

@ -1,7 +1,7 @@
<template> <template>
<button class="dmtdnykelhudezerjlfpbhgovrgnqqgr" :class="[styl, { inline, primary }]" :type="type" @click="$emit('click')"> <component class="dmtdnykelhudezerjlfpbhgovrgnqqgr" :is="link ? 'a' : 'button'" :class="[styl, { inline, primary }]" :type="type" @click="$emit('click')">
<slot></slot> <slot></slot>
</button> </component>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -21,6 +21,11 @@ export default Vue.extend({
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
},
link: {
type: Boolean,
required: false,
default: false
} }
}, },
data() { data() {
@ -35,15 +40,20 @@ export default Vue.extend({
.dmtdnykelhudezerjlfpbhgovrgnqqgr .dmtdnykelhudezerjlfpbhgovrgnqqgr
display block display block
width 100% width 100%
min-height 40px
margin 0 margin 0
padding 0 padding 8px
text-align center
font-weight normal font-weight normal
font-size 16px font-size 16px
border none border none
border-radius 6px border-radius 6px
outline none outline none
box-shadow none box-shadow none
text-decoration none
user-select none
*
pointer-events none
&:focus &:focus
&:after &:after

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="qldxjjsrseehkusjuoooapmsprvfrxyl mk-admin-card"> <div class="qldxjjsrseehkusjuoooapmsprvfrxyl mk-admin-card">
<header>%i18n:@announcements%</header> <header>%i18n:@announcements%</header>
<textarea v-model="broadcasts"></textarea> <textarea v-model="broadcasts" placeholder='[ { "title": "Title1", "text": "Text1" }, { "title": "Title2", "text": "Text2" } ]'></textarea>
<button class="ui" @click="save">%i18n:@save%</button> <button class="ui" @click="save">%i18n:@save%</button>
</div> </div>
</template> </template>
@ -22,8 +22,21 @@ export default Vue.extend({
}, },
methods: { methods: {
save() { save() {
let json;
try {
json = JSON.parse(this.broadcasts);
} catch (e) {
(this as any).os.apis.dialog({ text: `Failed: ${e}` });
return;
}
(this as any).api('admin/update-meta', { (this as any).api('admin/update-meta', {
broadcasts: JSON.parse(this.broadcasts) broadcasts: json
}).then(() => {
(this as any).os.apis.dialog({ text: `Saved` });
}.catch(e => {
(this as any).os.apis.dialog({ text: `Failed ${e}` });
}); });
} }
} }

View File

@ -13,7 +13,7 @@
<x-cpu-memory :connection="connection"/> <x-cpu-memory :connection="connection"/>
</div> </div>
<div class="form"> <div v-if="this.$store.state.i && this.$store.state.i.isAdmin" class="form">
<div> <div>
<label> <label>
<p>%i18n:@banner-url%</p> <p>%i18n:@banner-url%</p>
@ -81,6 +81,8 @@ export default Vue.extend({
invite() { invite() {
(this as any).api('admin/invite').then(x => { (this as any).api('admin/invite').then(x => {
this.inviteCode = x.code; this.inviteCode = x.code;
}).catch(e => {
(this as any).os.apis.dialog({ text: `Failed ${e}` });
}); });
}, },
updateMeta() { updateMeta() {
@ -88,6 +90,10 @@ export default Vue.extend({
disableRegistration: this.disableRegistration, disableRegistration: this.disableRegistration,
disableLocalTimeline: this.disableLocalTimeline, disableLocalTimeline: this.disableLocalTimeline,
bannerUrl: this.bannerUrl bannerUrl: this.bannerUrl
}).then(() => {
(this as any).os.apis.dialog({ text: `Saved` });
}).catch(e => {
(this as any).os.apis.dialog({ text: `Failed ${e}` });
}); });
} }
} }

View File

@ -24,6 +24,10 @@ export default Vue.extend({
save() { save() {
(this as any).api('admin/update-meta', { (this as any).api('admin/update-meta', {
hidedTags: this.hidedTags.split('\n') hidedTags: this.hidedTags.split('\n')
}).then(() => {
(this as any).os.apis.dialog({ text: `Saved` });
}).catch(e => {
(this as any).os.apis.dialog({ text: `Failed ${e}` });
}); });
} }
} }

View File

@ -21,18 +21,24 @@ export default Vue.extend({
async suspendUser() { async suspendUser() {
this.suspending = true; this.suspending = true;
const user = await (this as any).os.api( const process = async () => {
"users/show", const user = await (this as any).os.api(
parseAcct(this.username) "users/show",
); parseAcct(this.username)
);
await (this as any).os.api("admin/suspend-user", { await (this as any).os.api("admin/suspend-user", {
userId: user.id userId: user.id
});
(this as any).os.apis.dialog({ text: "%i18n:@suspended%" });
};
await process().catch(e => {
(this as any).os.apis.dialog({ text: `Failed: ${e}` });
}); });
this.suspending = false; this.suspending = false;
(this as any).os.apis.dialog({ text: "%i18n:@suspended%" });
} }
} }
}); });

View File

@ -21,18 +21,25 @@ export default Vue.extend({
async unsuspendUser() { async unsuspendUser() {
this.unsuspending = true; this.unsuspending = true;
const user = await (this as any).os.api( const process = async () => {
"users/show", const user = await (this as any).os.api(
parseAcct(this.username) "users/show",
); parseAcct(this.username)
);
await (this as any).os.api("admin/unsuspend-user", { await (this as any).os.api("admin/unsuspend-user", {
userId: user.id userId: user.id
});
(this as any).os.apis.dialog({ text: "%i18n:@unsuspended%" });
};
await process().catch(e => {
(this as any).os.apis.dialog({ text: `Failed: ${e}` });
}); });
this.unsuspending = false; this.unsuspending = false;
(this as any).os.apis.dialog({ text: "%i18n:@unsuspended%" });
} }
} }
}); });

View File

@ -21,18 +21,24 @@ export default Vue.extend({
async unverifyUser() { async unverifyUser() {
this.unverifying = true; this.unverifying = true;
const user = await (this as any).os.api( const process = async () => {
"users/show", const user = await (this as any).os.api(
parseAcct(this.username) "users/show",
); parseAcct(this.username)
);
await (this as any).os.api("admin/unverify-user", { await (this as any).os.api("admin/unverify-user", {
userId: user.id userId: user.id
});
(this as any).os.apis.dialog({ text: "%i18n:@unverified%" });
};
await process().catch(e => {
(this as any).os.apis.dialog({ text: `Failed: ${e}` });
}); });
this.unverifying = false; this.unverifying = false;
(this as any).os.apis.dialog({ text: "%i18n:@unverified%" });
} }
} }
}); });

View File

@ -21,18 +21,24 @@ export default Vue.extend({
async verifyUser() { async verifyUser() {
this.verifying = true; this.verifying = true;
const user = await (this as any).os.api( const process = async () => {
"users/show", const user = await (this as any).os.api(
parseAcct(this.username) "users/show",
); parseAcct(this.username)
);
await (this as any).os.api("admin/verify-user", { await (this as any).os.api("admin/verify-user", {
userId: user.id userId: user.id
});
(this as any).os.apis.dialog({ text: "%i18n:@verified%" });
};
await process().catch(e => {
(this as any).os.apis.dialog({ text: `Failed: ${e}` });
}); });
this.verifying = false; this.verifying = false;
(this as any).os.apis.dialog({ text: "%i18n:@verified%" });
} }
} }
}); });

View File

@ -3,9 +3,15 @@
<nav> <nav>
<ul> <ul>
<li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }">%fa:chalkboard .fw%%i18n:@dashboard%</li> <li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }">%fa:chalkboard .fw%%i18n:@dashboard%</li>
<li @click="nav('users')" :class="{ active: page == 'users' }">%fa:users .fw%%i18n:@users%</li>
<li @click="nav('announcements')" :class="{ active: page == 'announcements' }">%fa:broadcast-tower .fw%%i18n:@announcements%</li> <li v-if="this.$store.state.i && this.$store.state.i.isAdmin"
<li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }">%fa:hashtag .fw%%i18n:@hashtags%</li> @click="nav('users')" :class="{ active: page == 'users' }">%fa:users .fw%%i18n:@users%</li>
<li v-if="this.$store.state.i && this.$store.state.i.isAdmin"
@click="nav('announcements')" :class="{ active: page == 'announcements' }">%fa:broadcast-tower .fw%%i18n:@announcements%</li>
<li v-if="this.$store.state.i && this.$store.state.i.isAdmin"
@click="nav('hashtags')" :class="{ active: page == 'hashtags' }">%fa:hashtag .fw%%i18n:@hashtags%</li>
<!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }">%fa:cloud .fw%%i18n:@drive%</li> --> <!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }">%fa:cloud .fw%%i18n:@drive%</li> -->
<!-- <li @click="nav('update')" :class="{ active: page == 'update' }">%i18n:@update%</li> --> <!-- <li @click="nav('update')" :class="{ active: page == 'update' }">%i18n:@update%</li> -->

View File

@ -14,8 +14,7 @@ import App from './app.vue';
import checkForUpdate from './common/scripts/check-for-update'; import checkForUpdate from './common/scripts/check-for-update';
import MiOS, { API } from './mios'; import MiOS, { API } from './mios';
import { version, codename, lang } from './config'; import { version, codename, lang } from './config';
import { builtinThemes, applyTheme } from './theme'; import { builtinThemes, lightTheme, applyTheme } from './theme';
const lightTheme = require('../theme/light.json');
if (localStorage.getItem('theme') == null) { if (localStorage.getItem('theme') == null) {
applyTheme(lightTheme); applyTheme(lightTheme);
@ -97,15 +96,15 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
return s.device.darkmode; return s.device.darkmode;
}, v => { }, v => {
const themes = os.store.state.device.themes.concat(builtinThemes); const themes = os.store.state.device.themes.concat(builtinThemes);
const dark = themes.find(t => t.meta.id == os.store.state.device.darkTheme); const dark = themes.find(t => t.id == os.store.state.device.darkTheme);
const light = themes.find(t => t.meta.id == os.store.state.device.lightTheme); const light = themes.find(t => t.id == os.store.state.device.lightTheme);
applyTheme(v ? dark : light); applyTheme(v ? dark : light);
}); });
os.store.watch(s => { os.store.watch(s => {
return s.device.lightTheme; return s.device.lightTheme;
}, v => { }, v => {
const themes = os.store.state.device.themes.concat(builtinThemes); const themes = os.store.state.device.themes.concat(builtinThemes);
const theme = themes.find(t => t.meta.id == v); const theme = themes.find(t => t.id == v);
if (!os.store.state.device.darkmode) { if (!os.store.state.device.darkmode) {
applyTheme(theme); applyTheme(theme);
} }
@ -114,7 +113,7 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
return s.device.darkTheme; return s.device.darkTheme;
}, v => { }, v => {
const themes = os.store.state.device.themes.concat(builtinThemes); const themes = os.store.state.device.themes.concat(builtinThemes);
const theme = themes.find(t => t.meta.id == v); const theme = themes.find(t => t.id == v);
if (os.store.state.device.darkmode) { if (os.store.state.device.darkmode) {
applyTheme(theme); applyTheme(theme);
} }

View File

@ -12,16 +12,6 @@ if (!('fetch' in window)) {
'To run Misskey, please update your browser to latest version or try other browsers.'); 'To run Misskey, please update your browser to latest version or try other browsers.');
} }
// Detect Edge
if (navigator.userAgent.toLowerCase().indexOf('edge') != -1) {
alert(
'現在、お使いのブラウザ(Microsoft Edge)ではMisskeyは正しく動作しません。' +
'サポートしているブラウザ: Google Chrome, Mozilla Firefox, Apple Safari など' +
'\n\n' +
'Currently, Misskey cannot run correctly on your browser (Microsoft Edge). ' +
'Supported browsers: Google Chrome, Mozilla Firefox, Apple Safari, etc');
}
// Check whether cookie enabled // Check whether cookie enabled
if (!navigator.cookieEnabled) { if (!navigator.cookieEnabled) {
alert( alert(

View File

@ -1,27 +1,40 @@
import * as tinycolor from 'tinycolor2'; import * as tinycolor from 'tinycolor2';
type Theme = { export type Theme = {
meta: { id: string;
id: string; name: string;
name: string; author: string;
author: string; desc?: string;
base?: string; base?: 'dark' | 'light';
vars: any; vars: { [key: string]: string };
}; props: { [key: string]: string };
} & {
[key: string]: string;
}; };
export const lightTheme: Theme = require('../theme/light.json5');
export const darkTheme: Theme = require('../theme/dark.json5');
export const pinkTheme: Theme = require('../theme/pink.json5');
export const halloweenTheme: Theme = require('../theme/halloween.json5');
export const builtinThemes = [
lightTheme,
darkTheme,
pinkTheme,
halloweenTheme
];
export function applyTheme(theme: Theme, persisted = true) { export function applyTheme(theme: Theme, persisted = true) {
if (theme.meta.base) { // Deep copy
const base = [lightTheme, darkTheme].find(x => x.meta.id == theme.meta.base); const _theme = JSON.parse(JSON.stringify(theme));
theme = Object.assign({}, base, theme);
if (_theme.base) {
const base = [lightTheme, darkTheme].find(x => x.id == _theme.base);
_theme.vars = Object.assign({}, base.vars, _theme.vars);
_theme.props = Object.assign({}, base.props, _theme.props);
} }
const props = compile(theme); const props = compile(_theme);
Object.entries(props).forEach(([k, v]) => { Object.entries(props).forEach(([k, v]) => {
if (k == 'meta') return;
document.documentElement.style.setProperty(`--${k}`, v.toString()); document.documentElement.style.setProperty(`--${k}`, v.toString());
}); });
@ -34,10 +47,10 @@ function compile(theme: Theme): { [key: string]: string } {
function getColor(code: string): tinycolor.Instance { function getColor(code: string): tinycolor.Instance {
// ref // ref
if (code[0] == '@') { if (code[0] == '@') {
return getColor(theme[code.substr(1)]); return getColor(theme.props[code.substr(1)]);
} }
if (code[0] == '$') { if (code[0] == '$') {
return getColor(theme.meta.vars[code.substr(1)]); return getColor(theme.vars[code.substr(1)]);
} }
// func // func
@ -59,8 +72,7 @@ function compile(theme: Theme): { [key: string]: string } {
const props = {}; const props = {};
Object.entries(theme).forEach(([k, v]) => { Object.entries(theme.props).forEach(([k, v]) => {
if (k == 'meta') return;
const c = getColor(v); const c = getColor(v);
props[k] = genValue(c); props[k] = genValue(c);
}); });
@ -88,15 +100,3 @@ function compile(theme: Theme): { [key: string]: string } {
function genValue(c: tinycolor.Instance): string { function genValue(c: tinycolor.Instance): string {
return c.toRgbString(); return c.toRgbString();
} }
export const lightTheme = require('../theme/light.json');
export const darkTheme = require('../theme/dark.json');
export const pinkTheme = require('../theme/pink.json');
export const halloweenTheme = require('../theme/halloween.json');
export const builtinThemes = [
lightTheme,
darkTheme,
pinkTheme,
halloweenTheme
];

View File

@ -1,93 +0,0 @@
.hljs {
font-family: Consolas, 'Courier New', Courier, Monaco, monospace;
}
.hljs,
.hljs-subst {
color: #444;
}
.hljs-comment {
color: #888888;
}
.hljs-keyword {
color: #2973b7;
}
.hljs-number {
color: #ae81ff;
}
.hljs-string {
color: #e96900;
}
.hljs-regexp {
color: #e9003f;
}
.hljs-attribute,
.hljs-selector-tag,
.hljs-meta-keyword,
.hljs-doctag,
.hljs-name {
font-weight: bold;
}
.hljs-type,
.hljs-selector-id,
.hljs-selector-class,
.hljs-quote,
.hljs-template-tag,
.hljs-deletion {
color: #880000;
}
.hljs-title,
.hljs-section {
color: #880000;
font-weight: bold;
}
.hljs-symbol,
.hljs-variable,
.hljs-template-variable,
.hljs-link,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #BC6060;
}
/* Language color: hue: 90; */
.hljs-literal {
color: #78A960;
}
.hljs-built_in,
.hljs-bullet,
.hljs-code,
.hljs-addition {
color: #397300;
}
/* Meta color: hue: 200 */
.hljs-meta {
color: #1f7199;
}
.hljs-meta-string {
color: #4d99bf;
}
/* Misc effects */
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

View File

@ -1,204 +0,0 @@
{
"meta": {
"id": "dark",
"name": "Dark",
"author": "syuilo",
"vars": {
"primary": "#fb4e4e",
"secondary": "#282C37",
"text": "#d6dae0"
}
},
"primary": "$primary",
"primaryForeground": "#fff",
"secondary": "$secondary",
"bg": ":darken<8<$secondary",
"text": "$text",
"scrollbarTrack": ":darken<5<$secondary",
"scrollbarHandle": ":lighten<5<$secondary",
"scrollbarHandleHover": ":lighten<10<$secondary",
"face": "$secondary",
"faceText": "#fff",
"faceHeader": ":lighten<5<$secondary",
"faceHeaderText": "#e3e5e8",
"faceDivider": "rgba(0, 0, 0, 0.3)",
"faceTextButton": "#9baec8",
"faceTextButtonHover": "#b2c1d5",
"faceTextButtonActive": "#b2c1d5",
"faceClearButtonHover": "rgba(0, 0, 0, 0.1)",
"faceClearButtonActive": "rgba(0, 0, 0, 0.2)",
"popupBg": ":lighten<5<$secondary",
"popupFg": "#d6dce2",
"subNoteBg": "rgba(0, 0, 0, 0.18)",
"subNoteText": ":alpha<0.7<$text",
"renoteGradient": "#314027",
"renoteText": "#9dbb00",
"quoteBorder": "#4e945e",
"noteText": "#fff",
"noteHeaderName": "#fff",
"noteHeaderBadgeFg": "#758188",
"noteHeaderBadgeBg": "rgba(0, 0, 0, 0.25)",
"noteHeaderAdminFg": "#f15f71",
"noteHeaderAdminBg": "#5d282e",
"noteHeaderAcct": "#606984",
"noteHeaderInfo": "#606984",
"noteActions": "#606984",
"noteActionsHover": "#a1a8bf",
"noteActionsReplyHover": "#0af",
"noteActionsRenoteHover": "#8d0",
"noteActionsReactionHover": "#fa0",
"noteActionsHighlighted": "#707b97",
"noteAttachedFile": "rgba(255, 255, 255, 0.1)",
"modalBackdrop": "rgba(0, 0, 0, 0.5)",
"dateDividerBg": ":darken<2<$secondary",
"dateDividerFg": ":alpha<0.7<$text",
"switchTrack": "rgba(255, 255, 255, 0.15)",
"radioBorder": "rgba(255, 255, 255, 0.6)",
"inputBorder": "rgba(255, 255, 255, 0.7)",
"inputLabel": "rgba(255, 255, 255, 0.7)",
"inputText": "#fff",
"buttonBg": "rgba(255, 255, 255, 0.05)",
"buttonHoverBg": "rgba(255, 255, 255, 0.1)",
"buttonActiveBg": "rgba(255, 255, 255, 0.15)",
"autocompleteItemHoverBg": "rgba(255, 255, 255, 0.1)",
"autocompleteItemText": "rgba(255, 255, 255, 0.8)",
"autocompleteItemTextSub": "rgba(255, 255, 255, 0.3)",
"cwButtonBg": "#687390",
"cwButtonFg": "#393f4f",
"cwButtonHoverBg": "#707b97",
"reactionPickerButtonHoverBg": "rgba(255, 255, 255, 0.18)",
"reactionViewerBorder": "rgba(255, 255, 255, 0.1)",
"pollEditorInputBg": "rgba(0, 0, 0, 0.25)",
"pollChoiceText": "#fff",
"pollChoiceBorder": "rgba(255, 255, 255, 0.1)",
"urlPreviewBorder": "rgba(0, 0, 0, 0.4)",
"urlPreviewBorderHover": "rgba(255, 255, 255, 0.2)",
"urlPreviewTitle": "$text",
"urlPreviewText": ":alpha<0.7<$text",
"urlPreviewInfo": ":alpha<0.8<$text",
"calendarWeek": "#43d5dc",
"calendarSaturdayOrSunday": "#ff6679",
"calendarDay": "#c5ced6",
"materBg": "rgba(0, 0, 0, 0.3)",
"chartCaption": ":alpha<0.6<$text",
"announcementsBg": "#253a50",
"announcementsTitle": "#539eff",
"announcementsText": "#fff",
"donationBg": "#5d5242",
"donationFg": "#e4dbce",
"googleSearchBg": "rgba(0, 0, 0, 0.2)",
"googleSearchFg": "#dee4e8",
"googleSearchBorder": "rgba(255, 255, 255, 0.2)",
"googleSearchHoverBorder": "rgba(255, 255, 255, 0.3)",
"googleSearchHoverButton": "rgba(255, 255, 255, 0.1)",
"mfmTitleBg": "rgba(0, 0, 0, 0.2)",
"mfmQuote": ":alpha<0.7<$text",
"mfmQuoteLine": ":alpha<0.6<$text",
"suspendedInfoBg": "#611d1d",
"suspendedInfoFg": "#ffb4b4",
"remoteInfoBg": "#42321c",
"remoteInfoFg": "#ffbd3e",
"messagingRoomBg": "@bg",
"messagingRoomInfo": "#fff",
"messagingRoomDateDividerLine": "rgba(255, 255, 255, 0.1)",
"messagingRoomDateDividerText": "rgba(255, 255, 255, 0.3)",
"messagingRoomMessageInfo": "rgba(255, 255, 255, 0.4)",
"messagingRoomMessageBg": "$secondary",
"messagingRoomMessageFg": "#fff",
"formButtonBorder": "rgba(255, 255, 255, 0.1)",
"formButtonHoverBg": ":alpha<0.2<$primary",
"formButtonHoverBorder": ":alpha<0.5<$primary",
"formButtonActiveBg": ":alpha<0.12<$primary",
"desktopHeaderBg": ":lighten<5<$secondary",
"desktopHeaderFg": "$text",
"desktopHeaderHoverFg": "#fff",
"desktopHeaderSearchBg": "rgba(0, 0, 0, 0.1)",
"desktopHeaderSearchHoverBg": "rgba(255, 255, 255, 0.04)",
"desktopHeaderSearchFg": "#fff",
"desktopNotificationBg": ":alpha<0.9<$secondary",
"desktopNotificationFg": ":alpha<0.7<$text",
"desktopNotificationShadow": "rgba(0, 0, 0, 0.4)",
"desktopPostFormBg": "@face",
"desktopPostFormTextareaBg": "rgba(0, 0, 0, 0.25)",
"desktopPostFormTextareaFg": "#fff",
"desktopPostFormTransparentButtonFg": "$primary",
"desktopPostFormTransparentButtonActiveGradientStart": ":darken<8<$secondary",
"desktopPostFormTransparentButtonActiveGradientEnd": ":darken<3<$secondary",
"desktopRenoteFormFooter": ":lighten<5<$secondary",
"desktopTimelineHeaderShadow": "rgba(0, 0, 0, 0.15)",
"desktopTimelineSrc": "@faceTextButton",
"desktopTimelineSrcHover": "@faceTextButtonHover",
"desktopWindowTitle": "@faceHeaderText",
"desktopWindowShadow": "rgba(0, 0, 0, 0.5)",
"desktopDriveBg": "@bg",
"desktopDriveFolderBg": ":alpha<0.2<$primary",
"desktopDriveFolderHoverBg": ":alpha<0.3<$primary",
"desktopDriveFolderActiveBg": ":alpha<0.3<:darken<10<$primary",
"desktopDriveFolderFg": "#fff",
"desktopSettingsNavItem": ":alpha<0.8<$text",
"desktopSettingsNavItemHover": ":lighten<10<$text",
"deckAcrylicColumnBg": "rgba(0, 0, 0, 0.25)",
"mobileHeaderBg": ":lighten<5<$secondary",
"mobileHeaderFg": "$text",
"mobileNavBackdrop": "rgba(0, 0, 0, 0.7)",
"mobilePostFormDivider": "rgba(0, 0, 0, 0.2)",
"mobilePostFormTextareaBg": "rgba(0, 0, 0, 0.3)",
"mobileDriveNavBg": ":alpha<0.75<$secondary",
"mobileHomeTlItemHover": "rgba(255, 255, 255, 0.1)",
"mobileUserPageName": "#fff",
"mobileUserPageAcct": "$text",
"mobileUserPageDescription": "$text",
"mobileUserPageFollowedBg": "rgba(0, 0, 0, 0.3)",
"mobileUserPageFollowedFg": "$text",
"mobileUserPageStatusHighlight": "#fff",
"mobileUserPageHeaderShadow": "rgba(0, 0, 0, 0.3)",
"mobileAnnouncement": "rgba(30, 129, 216, 0.2)",
"mobileAnnouncementFg": "#fff",
"mobileSignedInAsBg": "#273c34",
"mobileSignedInAsFg": "#49ab63",
"mobileSignoutBg": "#652222",
"mobileSignoutFg": "#ff5f56",
"reversiBannerGradientStart": "#45730e",
"reversiBannerGradientEnd": "#464300",
"reversiDescBg": "rgba(255, 255, 255, 0.1)",
"reversiListItemShadow": "rgba(0, 0, 0, 0.7)",
"reversiMapSelectBorder": "rgba(255, 255, 255, 0.1)",
"reversiMapSelectHoverBorder": "rgba(255, 255, 255, 0.2)",
"reversiRoomFormShadow": "rgba(0, 0, 0, 0.7)",
"reversiRoomFooterBg": ":alpha<0.9<$secondary",
"reversiGameHeaderLine": ":alpha<0.5<$secondary",
"reversiGameEmptyCell": ":lighten<2<$secondary",
"reversiGameEmptyCellMyTurn": ":lighten<5<$secondary",
"reversiGameEmptyCellCanPut": ":lighten<4<$secondary"
}

207
src/client/theme/dark.json5 Normal file
View File

@ -0,0 +1,207 @@
{
id: 'dark',
name: 'Dark',
author: 'syuilo',
desc: 'Default dark theme',
vars: {
primary: '#fb4e4e',
secondary: '#282C37',
text: '#d6dae0',
},
props: {
primary: '$primary',
primaryForeground: '#fff',
secondary: '$secondary',
bg: ':darken<8<$secondary',
text: '$text',
scrollbarTrack: ':darken<5<$secondary',
scrollbarHandle: ':lighten<5<$secondary',
scrollbarHandleHover: ':lighten<10<$secondary',
face: '$secondary',
faceText: '#fff',
faceHeader: ':lighten<5<$secondary',
faceHeaderText: '#e3e5e8',
faceDivider: 'rgba(0, 0, 0, 0.3)',
faceTextButton: '$text',
faceTextButtonHover: ':lighten<10<$text',
faceTextButtonActive: ':darken<10<$text',
faceClearButtonHover: 'rgba(0, 0, 0, 0.1)',
faceClearButtonActive: 'rgba(0, 0, 0, 0.2)',
popupBg: ':lighten<5<$secondary',
popupFg: '#d6dce2',
subNoteBg: 'rgba(0, 0, 0, 0.18)',
subNoteText: ':alpha<0.7<$text',
renoteGradient: '#314027',
renoteText: '#9dbb00',
quoteBorder: '#4e945e',
noteText: '#fff',
noteHeaderName: '#fff',
noteHeaderBadgeFg: '#758188',
noteHeaderBadgeBg: 'rgba(0, 0, 0, 0.25)',
noteHeaderAdminFg: '#f15f71',
noteHeaderAdminBg: '#5d282e',
noteHeaderAcct: ':alpha<0.65<$text',
noteHeaderInfo: ':alpha<0.5<$text',
noteActions: ':alpha<0.45<$text',
noteActionsHover: ':alpha<0.6<$text',
noteActionsReplyHover: '#0af',
noteActionsRenoteHover: '#8d0',
noteActionsReactionHover: '#fa0',
noteActionsHighlighted: ':alpha<0.7<$text',
noteAttachedFile: 'rgba(255, 255, 255, 0.1)',
modalBackdrop: 'rgba(0, 0, 0, 0.5)',
dateDividerBg: ':darken<2<$secondary',
dateDividerFg: ':alpha<0.7<$text',
switchTrack: 'rgba(255, 255, 255, 0.15)',
radioBorder: 'rgba(255, 255, 255, 0.6)',
inputBorder: 'rgba(255, 255, 255, 0.7)',
inputLabel: 'rgba(255, 255, 255, 0.7)',
inputText: '#fff',
buttonBg: 'rgba(255, 255, 255, 0.05)',
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
buttonActiveBg: 'rgba(255, 255, 255, 0.15)',
autocompleteItemHoverBg: 'rgba(255, 255, 255, 0.1)',
autocompleteItemText: 'rgba(255, 255, 255, 0.8)',
autocompleteItemTextSub: 'rgba(255, 255, 255, 0.3)',
cwButtonBg: '#687390',
cwButtonFg: '#393f4f',
cwButtonHoverBg: '#707b97',
reactionPickerButtonHoverBg: 'rgba(255, 255, 255, 0.18)',
reactionViewerBorder: 'rgba(255, 255, 255, 0.1)',
pollEditorInputBg: 'rgba(0, 0, 0, 0.25)',
pollChoiceText: '#fff',
pollChoiceBorder: 'rgba(255, 255, 255, 0.1)',
urlPreviewBorder: 'rgba(0, 0, 0, 0.4)',
urlPreviewBorderHover: 'rgba(255, 255, 255, 0.2)',
urlPreviewTitle: '$text',
urlPreviewText: ':alpha<0.7<$text',
urlPreviewInfo: ':alpha<0.8<$text',
calendarWeek: '#43d5dc',
calendarSaturdayOrSunday: '#ff6679',
calendarDay: '#c5ced6',
materBg: 'rgba(0, 0, 0, 0.3)',
chartCaption: ':alpha<0.6<$text',
announcementsBg: '#253a50',
announcementsTitle: '#539eff',
announcementsText: '#fff',
donationBg: '#5d5242',
donationFg: '#e4dbce',
googleSearchBg: 'rgba(0, 0, 0, 0.2)',
googleSearchFg: '#dee4e8',
googleSearchBorder: 'rgba(255, 255, 255, 0.2)',
googleSearchHoverBorder: 'rgba(255, 255, 255, 0.3)',
googleSearchHoverButton: 'rgba(255, 255, 255, 0.1)',
mfmTitleBg: 'rgba(0, 0, 0, 0.2)',
mfmQuote: ':alpha<0.7<$text',
mfmQuoteLine: ':alpha<0.6<$text',
suspendedInfoBg: '#611d1d',
suspendedInfoFg: '#ffb4b4',
remoteInfoBg: '#42321c',
remoteInfoFg: '#ffbd3e',
messagingRoomBg: '@bg',
messagingRoomInfo: '#fff',
messagingRoomDateDividerLine: 'rgba(255, 255, 255, 0.1)',
messagingRoomDateDividerText: 'rgba(255, 255, 255, 0.3)',
messagingRoomMessageInfo: 'rgba(255, 255, 255, 0.4)',
messagingRoomMessageBg: '$secondary',
messagingRoomMessageFg: '#fff',
formButtonBorder: 'rgba(255, 255, 255, 0.1)',
formButtonHoverBg: ':alpha<0.2<$primary',
formButtonHoverBorder: ':alpha<0.5<$primary',
formButtonActiveBg: ':alpha<0.12<$primary',
desktopHeaderBg: ':lighten<5<$secondary',
desktopHeaderFg: '$text',
desktopHeaderHoverFg: '#fff',
desktopHeaderSearchBg: 'rgba(0, 0, 0, 0.1)',
desktopHeaderSearchHoverBg: 'rgba(255, 255, 255, 0.04)',
desktopHeaderSearchFg: '#fff',
desktopNotificationBg: ':alpha<0.9<$secondary',
desktopNotificationFg: ':alpha<0.7<$text',
desktopNotificationShadow: 'rgba(0, 0, 0, 0.4)',
desktopPostFormBg: '@face',
desktopPostFormTextareaBg: 'rgba(0, 0, 0, 0.25)',
desktopPostFormTextareaFg: '#fff',
desktopPostFormTransparentButtonFg: '$primary',
desktopPostFormTransparentButtonActiveGradientStart: ':darken<8<$secondary',
desktopPostFormTransparentButtonActiveGradientEnd: ':darken<3<$secondary',
desktopRenoteFormFooter: ':lighten<5<$secondary',
desktopTimelineHeaderShadow: 'rgba(0, 0, 0, 0.15)',
desktopTimelineSrc: '@faceTextButton',
desktopTimelineSrcHover: '@faceTextButtonHover',
desktopWindowTitle: '@faceHeaderText',
desktopWindowShadow: 'rgba(0, 0, 0, 0.5)',
desktopDriveBg: '@bg',
desktopDriveFolderBg: ':alpha<0.2<$primary',
desktopDriveFolderHoverBg: ':alpha<0.3<$primary',
desktopDriveFolderActiveBg: ':alpha<0.3<:darken<10<$primary',
desktopDriveFolderFg: '#fff',
desktopSettingsNavItem: ':alpha<0.8<$text',
desktopSettingsNavItemHover: ':lighten<10<$text',
deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.25)',
mobileHeaderBg: ':lighten<5<$secondary',
mobileHeaderFg: '$text',
mobileNavBackdrop: 'rgba(0, 0, 0, 0.7)',
mobilePostFormDivider: 'rgba(0, 0, 0, 0.2)',
mobilePostFormTextareaBg: 'rgba(0, 0, 0, 0.3)',
mobileDriveNavBg: ':alpha<0.75<$secondary',
mobileHomeTlItemHover: 'rgba(255, 255, 255, 0.1)',
mobileUserPageName: '#fff',
mobileUserPageAcct: '$text',
mobileUserPageDescription: '$text',
mobileUserPageFollowedBg: 'rgba(0, 0, 0, 0.3)',
mobileUserPageFollowedFg: '$text',
mobileUserPageStatusHighlight: '#fff',
mobileUserPageHeaderShadow: 'rgba(0, 0, 0, 0.3)',
mobileAnnouncement: 'rgba(30, 129, 216, 0.2)',
mobileAnnouncementFg: '#fff',
mobileSignedInAsBg: '#273c34',
mobileSignedInAsFg: '#49ab63',
mobileSignoutBg: '#652222',
mobileSignoutFg: '#ff5f56',
reversiBannerGradientStart: '#45730e',
reversiBannerGradientEnd: '#464300',
reversiDescBg: 'rgba(255, 255, 255, 0.1)',
reversiListItemShadow: 'rgba(0, 0, 0, 0.7)',
reversiMapSelectBorder: 'rgba(255, 255, 255, 0.1)',
reversiMapSelectHoverBorder: 'rgba(255, 255, 255, 0.2)',
reversiRoomFormShadow: 'rgba(0, 0, 0, 0.7)',
reversiRoomFooterBg: ':alpha<0.9<$secondary',
reversiGameHeaderLine: ':alpha<0.5<$secondary',
reversiGameEmptyCell: ':lighten<2<$secondary',
reversiGameEmptyCellMyTurn: ':lighten<5<$secondary',
reversiGameEmptyCellCanPut: ':lighten<4<$secondary',
},
}

View File

@ -1,17 +0,0 @@
{
"meta": {
"id": "42e4f09b-67d5-498c-af7d-29faa54745b0",
"name": "Halloween",
"author": "syuilo",
"base": "dark",
"vars": {
"primary": "#d67036",
"secondary": "#1f1d30",
"text": "#b1bee3"
}
},
"renoteGradient": "#5d2d1a",
"renoteText": "#ff6c00",
"quoteBorder": "#c3631c"
}

View File

@ -0,0 +1,21 @@
{
id: '42e4f09b-67d5-498c-af7d-29faa54745b0',
name: 'Halloween',
author: 'syuilo',
desc: 'Hello, Happy Halloween!',
base: 'dark',
vars: {
primary: '#d67036',
secondary: '#1f1d30',
text: '#b1bee3',
},
props: {
renoteGradient: '#5d2d1a',
renoteText: '#ff6c00',
quoteBorder: '#c3631c',
},
}

View File

@ -1,204 +0,0 @@
{
"meta": {
"id": "light",
"name": "Light",
"author": "syuilo",
"vars": {
"primary": "#fb4e4e",
"secondary": "#fff",
"text": "#666"
}
},
"primary": "$primary",
"primaryForeground": "#fff",
"secondary": "$secondary",
"bg": ":darken<8<$secondary",
"text": "$text",
"scrollbarTrack": "#fff",
"scrollbarHandle": "#00000033",
"scrollbarHandleHover": "#00000066",
"face": "$secondary",
"faceText": "#444",
"faceHeader": ":lighten<5<$secondary",
"faceHeaderText": "#888",
"faceDivider": "rgba(0, 0, 0, 0.082)",
"faceTextButton": "#ccc",
"faceTextButtonHover": "#aaa",
"faceTextButtonActive": "#999",
"faceClearButtonHover": "rgba(0, 0, 0, 0.025)",
"faceClearButtonActive": "rgba(0, 0, 0, 0.05)",
"popupBg": ":lighten<5<$secondary",
"popupFg": "#586069",
"subNoteBg": "rgba(0, 0, 0, 0.01)",
"subNoteText": ":alpha<0.7<$text",
"renoteGradient": "#edfde2",
"renoteText": "#9dbb00",
"quoteBorder": "#c0dac6",
"noteText": "#717171",
"noteHeaderName": ":darken<2<$text",
"noteHeaderBadgeFg": "#aaa",
"noteHeaderBadgeBg": "rgba(0, 0, 0, 0.05)",
"noteHeaderAdminFg": "#f15f71",
"noteHeaderAdminBg": "#ffdfdf",
"noteHeaderAcct": ":alpha<0.7<@noteHeaderName",
"noteHeaderInfo": ":alpha<0.7<@noteHeaderName",
"noteActions": ":alpha<0.3<$text",
"noteActionsHover": ":alpha<0.9<$text",
"noteActionsReplyHover": "#0af",
"noteActionsRenoteHover": "#8d0",
"noteActionsReactionHover": "#fa0",
"noteActionsHighlighted": "#888",
"noteAttachedFile": "rgba(0, 0, 0, 0.05)",
"modalBackdrop": "rgba(0, 0, 0, 0.1)",
"dateDividerBg": ":darken<2<$secondary",
"dateDividerFg": ":alpha<0.7<$text",
"switchTrack": "rgba(0, 0, 0, 0.25)",
"radioBorder": "rgba(0, 0, 0, 0.4)",
"inputBorder": "rgba(0, 0, 0, 0.42)",
"inputLabel": "rgba(0, 0, 0, 0.54)",
"inputText": "#000",
"buttonBg": "rgba(0, 0, 0, 0.05)",
"buttonHoverBg": "rgba(0, 0, 0, 0.1)",
"buttonActiveBg": "rgba(0, 0, 0, 0.15)",
"autocompleteItemHoverBg": "rgba(0, 0, 0, 0.1)",
"autocompleteItemText": "rgba(0, 0, 0, 0.8)",
"autocompleteItemTextSub": "rgba(0, 0, 0, 0.3)",
"cwButtonBg": "#b1b9c1",
"cwButtonFg": "#fff",
"cwButtonHoverBg": "#bbc4ce",
"reactionPickerButtonHoverBg": "#eee",
"reactionViewerBorder": "rgba(0, 0, 0, 0.1)",
"pollEditorInputBg": "#fff",
"pollChoiceText": "#000",
"pollChoiceBorder": "rgba(0, 0, 0, 0.1)",
"urlPreviewBorder": "rgba(0, 0, 0, 0.1)",
"urlPreviewBorderHover": "rgba(0, 0, 0, 0.2)",
"urlPreviewTitle": "$text",
"urlPreviewText": ":alpha<0.7<$text",
"urlPreviewInfo": ":alpha<0.8<$text",
"calendarWeek": "#19a2a9",
"calendarSaturdayOrSunday": "#ef95a0",
"calendarDay": "#777",
"materBg": "rgba(0, 0, 0, 0.1)",
"chartCaption": ":alpha<0.6<$text",
"announcementsBg": "#f3f9ff",
"announcementsTitle": "#4078c0",
"announcementsText": "#57616f",
"donationBg": "#fbead4",
"donationFg": "#777d71",
"googleSearchBg": "#fff",
"googleSearchFg": "#55595c",
"googleSearchBorder": "rgba(0, 0, 0, 0.2)",
"googleSearchHoverBorder": "rgba(0, 0, 0, 0.3)",
"googleSearchHoverButton": "rgba(0, 0, 0, 0.05)",
"mfmTitleBg": "rgba(0, 0, 0, 0.07)",
"mfmQuote": ":alpha<0.6<$text",
"mfmQuoteLine": ":alpha<0.5<$text",
"suspendedInfoBg": "#ffdbdb",
"suspendedInfoFg": "#570808",
"remoteInfoBg": "#fff0db",
"remoteInfoFg": "#573c08",
"messagingRoomBg": "#fff",
"messagingRoomInfo": "#000",
"messagingRoomDateDividerLine": "rgba(0, 0, 0, 0.1)",
"messagingRoomDateDividerText": "rgba(0, 0, 0, 0.3)",
"messagingRoomMessageInfo": "rgba(0, 0, 0, 0.4)",
"messagingRoomMessageBg": "#eee",
"messagingRoomMessageFg": "#333",
"formButtonBorder": "rgba(0, 0, 0, 0.1)",
"formButtonHoverBg": ":alpha<0.12<$primary",
"formButtonHoverBorder": ":alpha<0.3<$primary",
"formButtonActiveBg": ":alpha<0.12<$primary",
"desktopHeaderBg": ":lighten<5<$secondary",
"desktopHeaderFg": "$text",
"desktopHeaderHoverFg": "#7b8c88",
"desktopHeaderSearchBg": "rgba(0, 0, 0, 0.05)",
"desktopHeaderSearchHoverBg": "rgba(0, 0, 0, 0.08)",
"desktopHeaderSearchFg": "#000",
"desktopNotificationBg": ":alpha<0.9<$secondary",
"desktopNotificationFg": ":alpha<0.7<$text",
"desktopNotificationShadow": "rgba(0, 0, 0, 0.2)",
"desktopPostFormBg": ":lighten<33<$primary",
"desktopPostFormTextareaBg": "#fff",
"desktopPostFormTextareaFg": "#333",
"desktopPostFormTransparentButtonFg": ":alpha<0.5<$primary",
"desktopPostFormTransparentButtonActiveGradientStart": ":lighten<30<$primary",
"desktopPostFormTransparentButtonActiveGradientEnd": ":lighten<33<$primary",
"desktopRenoteFormFooter": ":lighten<33<$primary",
"desktopTimelineHeaderShadow": "rgba(0, 0, 0, 0.08)",
"desktopTimelineSrc": "#6f7477",
"desktopTimelineSrcHover": "#525a5f",
"desktopWindowTitle": "#666",
"desktopWindowShadow": "rgba(0, 0, 0, 0.2)",
"desktopDriveBg": "#fff",
"desktopDriveFolderBg": ":lighten<31<$primary",
"desktopDriveFolderHoverBg": ":lighten<27<$primary",
"desktopDriveFolderActiveBg": ":lighten<25<$primary",
"desktopDriveFolderFg": ":darken<10<$primary",
"desktopSettingsNavItem": ":alpha<0.8<$text",
"desktopSettingsNavItemHover": ":darken<10<$text",
"deckAcrylicColumnBg": "rgba(0, 0, 0, 0.1)",
"mobileHeaderBg": ":lighten<5<$secondary",
"mobileHeaderFg": "$text",
"mobileNavBackdrop": "rgba(0, 0, 0, 0.2)",
"mobilePostFormDivider": "rgba(0, 0, 0, 0.1)",
"mobilePostFormTextareaBg": "#fff",
"mobileDriveNavBg": ":alpha<0.75<$secondary",
"mobileHomeTlItemHover": "rgba(0, 0, 0, 0.05)",
"mobileUserPageName": "#757c82",
"mobileUserPageAcct": "#969ea5",
"mobileUserPageDescription": "#757c82",
"mobileUserPageFollowedBg": "#a7bec7",
"mobileUserPageFollowedFg": "#fff",
"mobileUserPageStatusHighlight": "#787e86",
"mobileUserPageHeaderShadow": "rgba(0, 0, 0, 0.07)",
"mobileAnnouncement": "rgba(155, 196, 232, 0.2)",
"mobileAnnouncementFg": "#3f4967",
"mobileSignedInAsBg": "#fcfff5",
"mobileSignedInAsFg": "#2c662d",
"mobileSignoutBg": "#fff6f5",
"mobileSignoutFg": "#cc2727",
"reversiBannerGradientStart": "#8bca3e",
"reversiBannerGradientEnd": "#d6cf31",
"reversiDescBg": "rgba(0, 0, 0, 0.1)",
"reversiListItemShadow": "rgba(0, 0, 0, 0.15)",
"reversiMapSelectBorder": "rgba(0, 0, 0, 0.1)",
"reversiMapSelectHoverBorder": "rgba(0, 0, 0, 0.2)",
"reversiRoomFormShadow": "rgba(0, 0, 0, 0.1)",
"reversiRoomFooterBg": ":alpha<0.9<$secondary",
"reversiGameHeaderLine": "#c4cdd4",
"reversiGameEmptyCell": "rgba(0, 0, 0, 0.06)",
"reversiGameEmptyCellMyTurn": "rgba(0, 0, 0, 0.12)",
"reversiGameEmptyCellCanPut": "rgba(0, 0, 0, 0.9)"
}

View File

@ -0,0 +1,207 @@
{
id: 'light',
name: 'Light',
author: 'syuilo',
desc: 'Default light theme',
vars: {
primary: '#fb4e4e',
secondary: '#fff',
text: '#666',
},
props: {
primary: '$primary',
primaryForeground: '#fff',
secondary: '$secondary',
bg: ':darken<8<$secondary',
text: '$text',
scrollbarTrack: '#fff',
scrollbarHandle: '#00000033',
scrollbarHandleHover: '#00000066',
face: '$secondary',
faceText: '$text',
faceHeader: ':lighten<5<$secondary',
faceHeaderText: '$text',
faceDivider: 'rgba(0, 0, 0, 0.082)',
faceTextButton: ':alpha<0.7<$text',
faceTextButtonHover: ':alpha<0.7<:darken<7<$text',
faceTextButtonActive: ':alpha<0.7<:darken<10<$text',
faceClearButtonHover: 'rgba(0, 0, 0, 0.025)',
faceClearButtonActive: 'rgba(0, 0, 0, 0.05)',
popupBg: ':lighten<5<$secondary',
popupFg: '#586069',
subNoteBg: 'rgba(0, 0, 0, 0.01)',
subNoteText: ':alpha<0.7<$text',
renoteGradient: '#edfde2',
renoteText: '#9dbb00',
quoteBorder: '#c0dac6',
noteText: '$text',
noteHeaderName: ':darken<2<$text',
noteHeaderBadgeFg: '#aaa',
noteHeaderBadgeBg: 'rgba(0, 0, 0, 0.05)',
noteHeaderAdminFg: '#f15f71',
noteHeaderAdminBg: '#ffdfdf',
noteHeaderAcct: ':alpha<0.7<@noteHeaderName',
noteHeaderInfo: ':alpha<0.7<@noteHeaderName',
noteActions: ':alpha<0.3<$text',
noteActionsHover: ':alpha<0.9<$text',
noteActionsReplyHover: '#0af',
noteActionsRenoteHover: '#8d0',
noteActionsReactionHover: '#fa0',
noteActionsHighlighted: '#888',
noteAttachedFile: 'rgba(0, 0, 0, 0.05)',
modalBackdrop: 'rgba(0, 0, 0, 0.1)',
dateDividerBg: ':darken<2<$secondary',
dateDividerFg: ':alpha<0.7<$text',
switchTrack: 'rgba(0, 0, 0, 0.25)',
radioBorder: 'rgba(0, 0, 0, 0.4)',
inputBorder: 'rgba(0, 0, 0, 0.42)',
inputLabel: 'rgba(0, 0, 0, 0.54)',
inputText: '#000',
buttonBg: 'rgba(0, 0, 0, 0.05)',
buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
buttonActiveBg: 'rgba(0, 0, 0, 0.15)',
autocompleteItemHoverBg: 'rgba(0, 0, 0, 0.1)',
autocompleteItemText: 'rgba(0, 0, 0, 0.8)',
autocompleteItemTextSub: 'rgba(0, 0, 0, 0.3)',
cwButtonBg: '#b1b9c1',
cwButtonFg: '#fff',
cwButtonHoverBg: '#bbc4ce',
reactionPickerButtonHoverBg: '#eee',
reactionViewerBorder: 'rgba(0, 0, 0, 0.1)',
pollEditorInputBg: '#fff',
pollChoiceText: '#000',
pollChoiceBorder: 'rgba(0, 0, 0, 0.1)',
urlPreviewBorder: 'rgba(0, 0, 0, 0.1)',
urlPreviewBorderHover: 'rgba(0, 0, 0, 0.2)',
urlPreviewTitle: '$text',
urlPreviewText: ':alpha<0.7<$text',
urlPreviewInfo: ':alpha<0.8<$text',
calendarWeek: '#19a2a9',
calendarSaturdayOrSunday: '#ef95a0',
calendarDay: '#777',
materBg: 'rgba(0, 0, 0, 0.1)',
chartCaption: ':alpha<0.6<$text',
announcementsBg: '#f3f9ff',
announcementsTitle: '#4078c0',
announcementsText: '#57616f',
donationBg: '#fbead4',
donationFg: '#777d71',
googleSearchBg: '#fff',
googleSearchFg: '#55595c',
googleSearchBorder: 'rgba(0, 0, 0, 0.2)',
googleSearchHoverBorder: 'rgba(0, 0, 0, 0.3)',
googleSearchHoverButton: 'rgba(0, 0, 0, 0.05)',
mfmTitleBg: 'rgba(0, 0, 0, 0.07)',
mfmQuote: ':alpha<0.6<$text',
mfmQuoteLine: ':alpha<0.5<$text',
suspendedInfoBg: '#ffdbdb',
suspendedInfoFg: '#570808',
remoteInfoBg: '#fff0db',
remoteInfoFg: '#573c08',
messagingRoomBg: '#fff',
messagingRoomInfo: '#000',
messagingRoomDateDividerLine: 'rgba(0, 0, 0, 0.1)',
messagingRoomDateDividerText: 'rgba(0, 0, 0, 0.3)',
messagingRoomMessageInfo: 'rgba(0, 0, 0, 0.4)',
messagingRoomMessageBg: '#eee',
messagingRoomMessageFg: '#333',
formButtonBorder: 'rgba(0, 0, 0, 0.1)',
formButtonHoverBg: ':alpha<0.12<$primary',
formButtonHoverBorder: ':alpha<0.3<$primary',
formButtonActiveBg: ':alpha<0.12<$primary',
desktopHeaderBg: ':lighten<5<$secondary',
desktopHeaderFg: '$text',
desktopHeaderHoverFg: ':darken<7<$text',
desktopHeaderSearchBg: 'rgba(0, 0, 0, 0.05)',
desktopHeaderSearchHoverBg: 'rgba(0, 0, 0, 0.08)',
desktopHeaderSearchFg: '#000',
desktopNotificationBg: ':alpha<0.9<$secondary',
desktopNotificationFg: ':alpha<0.7<$text',
desktopNotificationShadow: 'rgba(0, 0, 0, 0.2)',
desktopPostFormBg: ':lighten<33<$primary',
desktopPostFormTextareaBg: '#fff',
desktopPostFormTextareaFg: '#333',
desktopPostFormTransparentButtonFg: ':alpha<0.5<$primary',
desktopPostFormTransparentButtonActiveGradientStart: ':lighten<30<$primary',
desktopPostFormTransparentButtonActiveGradientEnd: ':lighten<33<$primary',
desktopRenoteFormFooter: ':lighten<33<$primary',
desktopTimelineHeaderShadow: 'rgba(0, 0, 0, 0.08)',
desktopTimelineSrc: '$text',
desktopTimelineSrcHover: ':darken<7<$text',
desktopWindowTitle: '$text',
desktopWindowShadow: 'rgba(0, 0, 0, 0.2)',
desktopDriveBg: '#fff',
desktopDriveFolderBg: ':lighten<31<$primary',
desktopDriveFolderHoverBg: ':lighten<27<$primary',
desktopDriveFolderActiveBg: ':lighten<25<$primary',
desktopDriveFolderFg: ':darken<10<$primary',
desktopSettingsNavItem: ':alpha<0.8<$text',
desktopSettingsNavItemHover: ':darken<10<$text',
deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.1)',
mobileHeaderBg: ':lighten<5<$secondary',
mobileHeaderFg: '$text',
mobileNavBackdrop: 'rgba(0, 0, 0, 0.2)',
mobilePostFormDivider: 'rgba(0, 0, 0, 0.1)',
mobilePostFormTextareaBg: '#fff',
mobileDriveNavBg: ':alpha<0.75<$secondary',
mobileHomeTlItemHover: 'rgba(0, 0, 0, 0.05)',
mobileUserPageName: '#757c82',
mobileUserPageAcct: '#969ea5',
mobileUserPageDescription: '#757c82',
mobileUserPageFollowedBg: '#a7bec7',
mobileUserPageFollowedFg: '#fff',
mobileUserPageStatusHighlight: '#787e86',
mobileUserPageHeaderShadow: 'rgba(0, 0, 0, 0.07)',
mobileAnnouncement: 'rgba(155, 196, 232, 0.2)',
mobileAnnouncementFg: '#3f4967',
mobileSignedInAsBg: '#fcfff5',
mobileSignedInAsFg: '#2c662d',
mobileSignoutBg: '#fff6f5',
mobileSignoutFg: '#cc2727',
reversiBannerGradientStart: '#8bca3e',
reversiBannerGradientEnd: '#d6cf31',
reversiDescBg: 'rgba(0, 0, 0, 0.1)',
reversiListItemShadow: 'rgba(0, 0, 0, 0.15)',
reversiMapSelectBorder: 'rgba(0, 0, 0, 0.1)',
reversiMapSelectHoverBorder: 'rgba(0, 0, 0, 0.2)',
reversiRoomFormShadow: 'rgba(0, 0, 0, 0.1)',
reversiRoomFooterBg: ':alpha<0.9<$secondary',
reversiGameHeaderLine: '#c4cdd4',
reversiGameEmptyCell: 'rgba(0, 0, 0, 0.06)',
reversiGameEmptyCellMyTurn: 'rgba(0, 0, 0, 0.12)',
reversiGameEmptyCellCanPut: 'rgba(0, 0, 0, 0.9)',
},
}

View File

@ -1,17 +0,0 @@
{
"meta": {
"id": "e9c8c01d-9c15-48d0-9b5c-3d00843b5b36",
"name": "Pink",
"author": "syuilo",
"base": "light",
"vars": {
"primary": "rgb(251, 78, 112)",
"secondary": "rgb(255, 218, 240)",
"text": "rgb(113, 91, 102)"
}
},
"renoteGradient": "#ffb1c9",
"renoteText": "#ff588d",
"quoteBorder": "#ff6c9b"
}

View File

@ -0,0 +1,20 @@
{
id: 'e9c8c01d-9c15-48d0-9b5c-3d00843b5b36',
name: 'Strawberry Milk',
author: 'syuilo',
base: 'light',
vars: {
primary: 'rgb(251, 78, 112)',
secondary: 'rgb(255, 218, 240)',
text: 'rgb(113, 91, 102)',
},
props: {
renoteGradient: '#ffb1c9',
renoteText: '#ff588d',
quoteBorder: '#ff6c9b',
},
}

View File

@ -127,6 +127,15 @@ export async function deleteDriveFile(driveFile: string | mongo.ObjectID | IDriv
}); });
} }
export const packMany = async (
files: any[],
options?: {
detail: boolean
}
) => {
return (await Promise.all(files.map(f => pack(f, options)))).filter(x => x != null);
};
/** /**
* Pack a drive file for API response * Pack a drive file for API response
*/ */
@ -155,7 +164,11 @@ export const pack = (
_file = deepcopy(file); _file = deepcopy(file);
} }
if (!_file) return reject('invalid file arg.'); // (データベースの欠損などで)ファイルがデータベース上に見つからなかったとき
if (_file == null) {
console.warn(`in packaging driveFile: driveFile not found on database: ${_file}`);
return null;
}
// rendered target // rendered target
let _target: any = {}; let _target: any = {};

View File

@ -41,6 +41,13 @@ export async function deleteFavorite(favorite: string | mongo.ObjectID | IFavori
}); });
} }
export const packMany = async (
favorites: any[],
me: any
) => {
return (await Promise.all(favorites.map(f => pack(f, me)))).filter(x => x != null);
};
/** /**
* Pack a favorite for API response * Pack a favorite for API response
*/ */
@ -70,5 +77,11 @@ export const pack = (
// Populate note // Populate note
_favorite.note = await packNote(_favorite.noteId, me); _favorite.note = await packNote(_favorite.noteId, me);
// (データベースの不具合などで)投稿が見つからなかったら
if (_favorite.note == null) {
console.warn(`in packaging favorite: note not found on database: ${_favorite.noteId}`);
return resolve(null);
}
resolve(_favorite); resolve(_favorite);
}); });

View File

@ -7,7 +7,7 @@ import { IUser, pack as packUser } from './user';
import { pack as packApp } from './app'; import { pack as packApp } from './app';
import PollVote, { deletePollVote } from './poll-vote'; import PollVote, { deletePollVote } from './poll-vote';
import Reaction, { deleteNoteReaction } from './note-reaction'; import Reaction, { deleteNoteReaction } from './note-reaction';
import { pack as packFile, IDriveFile } from './drive-file'; import { packMany as packFileMany, IDriveFile } from './drive-file';
import NoteWatching, { deleteNoteWatching } from './note-watching'; import NoteWatching, { deleteNoteWatching } from './note-watching';
import NoteReaction from './note-reaction'; import NoteReaction from './note-reaction';
import Favorite, { deleteFavorite } from './favorite'; import Favorite, { deleteFavorite } from './favorite';
@ -226,6 +226,17 @@ export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => {
} }
}; };
export const packMany = async (
notes: (string | mongo.ObjectID | INote)[],
me?: string | mongo.ObjectID | IUser,
options?: {
detail?: boolean;
skipHide?: boolean;
}
) => {
return (await Promise.all(notes.map(n => pack(n, me, options)))).filter(x => x != null);
};
/** /**
* Pack a note for API response * Pack a note for API response
* *
@ -271,7 +282,11 @@ export const pack = async (
_note = deepcopy(note); _note = deepcopy(note);
} }
if (!_note) throw `invalid note arg ${note}`; // 投稿がデータベース上に見つからなかったとき
if (_note == null) {
console.warn(`note not found on database: ${note}`);
return null;
}
const id = _note._id; const id = _note._id;
@ -294,9 +309,7 @@ export const pack = async (
} }
// Populate files // Populate files
_note.files = Promise.all(_note.fileIds.map((fileId: mongo.ObjectID) => _note.files = packFileMany(_note.fileIds || []);
packFile(fileId)
));
// 後方互換性のため // 後方互換性のため
_note.mediaIds = _note.fileIds; _note.mediaIds = _note.fileIds;
@ -365,6 +378,12 @@ export const pack = async (
// resolve promises in _note object // resolve promises in _note object
_note = await rap(_note); _note = await rap(_note);
// (データベースの欠損などで)ユーザーがデータベース上に見つからなかったとき
if (_note.user == null) {
console.warn(`in packaging note: note user not found on database: note(${_note.id})`);
return null;
}
if (_note.user.isCat && _note.text) { if (_note.user.isCat && _note.text) {
_note.text = _note.text.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ'); _note.text = _note.text.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ');
} }

View File

@ -77,6 +77,12 @@ export async function deleteNotification(notification: string | mongo.ObjectID |
}); });
} }
export const packMany = async (
notifications: any[]
) => {
return (await Promise.all(notifications.map(n => pack(n)))).filter(x => x != null);
};
/** /**
* Pack a notification for API response * Pack a notification for API response
*/ */
@ -123,6 +129,12 @@ export const pack = (notification: any) => new Promise<any>(async (resolve, reje
case 'poll_vote': case 'poll_vote':
// Populate note // Populate note
_notification.note = await packNote(_notification.noteId, me); _notification.note = await packNote(_notification.noteId, me);
// (データベースの不具合などで)投稿が見つからなかったら
if (_notification.note == null) {
console.warn(`in packaging notification: note not found on database: ${_notification.noteId}`);
return resolve(null);
}
break; break;
default: default:
console.error(`Unknown type: ${_notification.type}`); console.error(`Unknown type: ${_notification.type}`);

View File

@ -3,7 +3,7 @@ const deepcopy = require('deepcopy');
const sequential = require('promise-sequential'); const sequential = require('promise-sequential');
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
import db from '../db/mongodb'; import db from '../db/mongodb';
import Note, { pack as packNote, deleteNote } from './note'; import Note, { packMany as packNoteMany, deleteNote } from './note';
import Following, { deleteFollowing } from './following'; import Following, { deleteFollowing } from './following';
import Mute, { deleteMute } from './mute'; import Mute, { deleteMute } from './mute';
import { getFriendIds } from '../server/api/common/get-friends'; import { getFriendIds } from '../server/api/common/get-friends';
@ -113,6 +113,7 @@ export interface ILocalUser extends IUserBase {
export interface IRemoteUser extends IUserBase { export interface IRemoteUser extends IUserBase {
inbox: string; inbox: string;
sharedInbox?: string; sharedInbox?: string;
featured?: string;
endpoints: string[]; endpoints: string[];
uri: string; uri: string;
url?: string; url?: string;
@ -360,9 +361,11 @@ export const pack = (
_user = deepcopy(user); _user = deepcopy(user);
} }
// TODO: ここでエラーにするのではなくダミーのユーザーデータを返す // (データベースの欠損などで)ユーザーデータベース上に見つからなかったとき
// SEE: https://github.com/syuilo/misskey/issues/1432 if (_user == null) {
if (!_user) return reject('invalid user arg.'); console.warn(`user not found on database: ${user}`);
return null;
}
// Me // Me
const meId: mongo.ObjectID = me const meId: mongo.ObjectID = me
@ -467,9 +470,9 @@ export const pack = (
if (opts.detail) { if (opts.detail) {
if (_user.pinnedNoteIds) { if (_user.pinnedNoteIds) {
// Populate pinned notes // Populate pinned notes
_user.pinnedNotes = Promise.all(_user.pinnedNoteIds.map((id: mongo.ObjectId) => packNote(id, meId, { _user.pinnedNotes = packNoteMany(_user.pinnedNoteIds, meId, {
detail: true detail: true
}))); });
} }
if (meId && !meId.equals(_user.id)) { if (meId && !meId.equals(_user.id)) {

View File

@ -7,19 +7,18 @@ export default async (job: bq.Job, done: any): Promise<void> => {
await request(job.data.user, job.data.to, job.data.content); await request(job.data.user, job.data.to, job.data.content);
done(); done();
} catch (res) { } catch (res) {
if (res == null || !res.hasOwnProperty('statusCode')) { if (res != null && res.hasOwnProperty('statusCode')) {
console.warn(`deliver failed (unknown): ${res}`); if (res.statusCode >= 400 && res.statusCode < 500) {
return done(); // HTTPステータスコード4xxはクライアントエラーであり、それはつまり
} // 何回再送しても成功することはないということなのでエラーにはしないでおく
done();
if (res.statusCode == null) return done(); } else {
if (res.statusCode >= 400 && res.statusCode < 500) { console.warn(`deliver failed: ${res.statusCode} ${res.statusMessage} to=${job.data.to}`);
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり done(res.statusMessage);
// 何回再送しても成功することはないということなのでエラーにはしないでおく }
done();
} else { } else {
console.warn(`deliver failed: ${res.statusMessage}`); console.warn(`deliver failed: ${res} to=${job.data.to}`);
done(res.statusMessage); done();
} }
} }
}; };

View File

@ -0,0 +1,22 @@
import { IRemoteUser } from '../../../../models/user';
import { IAdd } from '../../type';
import { resolveNote } from '../../models/note';
import { addPinned } from '../../../../services/i/pin';
export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
if (activity.target == null) {
throw new Error('target is null');
}
if (activity.target === actor.featured) {
const note = await resolveNote(activity.object);
await addPinned(actor, note._id);
return;
}
throw new Error(`unknown target: ${activity.target}`);
};

View File

@ -8,6 +8,8 @@ import like from './like';
import announce from './announce'; import announce from './announce';
import accept from './accept'; import accept from './accept';
import reject from './reject'; import reject from './reject';
import add from './add';
import remove from './remove';
const self = async (actor: IRemoteUser, activity: Object): Promise<void> => { const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
switch (activity.type) { switch (activity.type) {
@ -31,6 +33,14 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
await reject(actor, activity); await reject(actor, activity);
break; break;
case 'Add':
await add(actor, activity).catch(err => console.log(err));
break;
case 'Remove':
await remove(actor, activity).catch(err => console.log(err));
break;
case 'Announce': case 'Announce':
await announce(actor, activity); await announce(actor, activity);
break; break;

View File

@ -0,0 +1,22 @@
import { IRemoteUser } from '../../../../models/user';
import { IRemove } from '../../type';
import { resolveNote } from '../../models/note';
import { removePinned } from '../../../../services/i/pin';
export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
if (activity.target == null) {
throw new Error('target is null');
}
if (activity.target === actor.featured) {
const note = await resolveNote(activity.object);
await removePinned(actor, note._id);
return;
}
throw new Error(`unknown target: ${activity.target}`);
};

View File

@ -56,7 +56,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
log(`Creating the Note: ${note.id}`); log(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ // 投稿者をフェッチ
const actor = await resolvePerson(note.attributedTo) as IRemoteUser; const actor = await resolvePerson(note.attributedTo, null, resolver) as IRemoteUser;
// 投稿者が凍結されていたらスキップ // 投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
@ -73,7 +73,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
visibility = 'followers'; visibility = 'followers';
} else { } else {
visibility = 'specified'; visibility = 'specified';
visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri))); visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, null, resolver)));
} }
} }
//#endergion //#endergion

View File

@ -3,15 +3,16 @@ import { toUnicode } from 'punycode';
import * as debug from 'debug'; import * as debug from 'debug';
import config from '../../../config'; import config from '../../../config';
import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user'; import User, { validateUsername, isValidName, IUser, IRemoteUser, isRemoteUser } from '../../../models/user';
import Resolver from '../resolver'; import Resolver from '../resolver';
import { resolveImage } from './image'; import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, IPerson } from '../type'; import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type';
import { IDriveFile } from '../../../models/drive-file'; import { IDriveFile } from '../../../models/drive-file';
import Meta from '../../../models/meta'; import Meta from '../../../models/meta';
import htmlToMFM from '../../../mfm/html-to-mfm'; import htmlToMFM from '../../../mfm/html-to-mfm';
import { updateUserStats } from '../../../services/update-chart'; import { updateUserStats } from '../../../services/update-chart';
import { URL } from 'url'; import { URL } from 'url';
import { resolveNote } from './note';
const log = debug('misskey:activitypub'); const log = debug('misskey:activitypub');
@ -155,6 +156,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
}, },
inbox: person.inbox, inbox: person.inbox,
sharedInbox: person.sharedInbox, sharedInbox: person.sharedInbox,
featured: person.featured,
endpoints: person.endpoints, endpoints: person.endpoints,
uri: person.id, uri: person.id,
url: person.url, url: person.url,
@ -211,6 +213,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
user.bannerUrl = bannerUrl; user.bannerUrl = bannerUrl;
//#endregion //#endregion
await updateFeatured(user._id).catch(err => console.log(err));
return user; return user;
} }
@ -282,6 +285,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
updatedAt: new Date(), updatedAt: new Date(),
inbox: person.inbox, inbox: person.inbox,
sharedInbox: person.sharedInbox, sharedInbox: person.sharedInbox,
featured: person.featured,
avatarId: avatar ? avatar._id : null, avatarId: avatar ? avatar._id : null,
bannerId: banner ? banner._id : null, bannerId: banner ? banner._id : null,
avatarUrl: (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null, avatarUrl: (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null,
@ -303,6 +307,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
}, },
} }
}); });
await updateFeatured(exist._id).catch(err => console.log(err));
} }
/** /**
@ -311,7 +317,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
* Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/ */
export async function resolvePerson(uri: string, verifier?: string): Promise<IUser> { export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<IUser> {
if (typeof uri !== 'string') throw 'uri is not string'; if (typeof uri !== 'string') throw 'uri is not string';
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
@ -323,5 +329,37 @@ export async function resolvePerson(uri: string, verifier?: string): Promise<IUs
//#endregion //#endregion
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録
return await createPerson(uri); if (resolver == null) resolver = new Resolver();
return await createPerson(uri, resolver);
}
export async function updateFeatured(userId: mongo.ObjectID) {
const user = await User.findOne({ _id: userId });
if (!isRemoteUser(user)) return;
if (!user.featured) return;
log(`Updating the featured: ${user.uri}`);
const resolver = new Resolver();
// Resolve to (Ordered)Collection Object
const collection = await resolver.resolveCollection(user.featured);
if (!isCollectionOrOrderedCollection(collection)) throw new Error(`Object is not Collection or OrderedCollection`);
// Resolve to Object(may be Note) arrays
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
const items = await resolver.resolve(unresolvedItems);
if (!Array.isArray(items)) throw new Error(`Collection items is not an array`);
// Resolve and regist Notes
const featuredNotes = await Promise.all(items
.filter(item => item.type === 'Note')
.slice(0, 5)
.map(item => resolveNote(item, resolver)));
await User.update({ _id: user._id }, {
$set: {
pinnedNoteIds: featuredNotes.map(note => note._id)
}
});
} }

View File

@ -12,6 +12,8 @@ const log = debug('misskey:activitypub:deliver');
export default (user: ILocalUser, url: string, object: any) => new Promise((resolve, reject) => { export default (user: ILocalUser, url: string, object: any) => new Promise((resolve, reject) => {
log(`--> ${url}`); log(`--> ${url}`);
const timeout = 10 * 1000;
const { protocol, hostname, port, pathname, search } = new URL(url); const { protocol, hostname, port, pathname, search } = new URL(url);
const data = JSON.stringify(object); const data = JSON.stringify(object);
@ -26,6 +28,7 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso
port, port,
method: 'POST', method: 'POST',
path: pathname + search, path: pathname + search,
timeout,
headers: { headers: {
'User-Agent': config.user_agent, 'User-Agent': config.user_agent,
'Content-Type': 'application/activity+json', 'Content-Type': 'application/activity+json',
@ -35,7 +38,7 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso
log(`${url} --> ${res.statusCode}`); log(`${url} --> ${res.statusCode}`);
if (res.statusCode >= 400) { if (res.statusCode >= 400) {
reject(); reject(res);
} else { } else {
resolve(); resolve();
} }
@ -53,5 +56,12 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso
sig = sig.replace(/^Signature /, ''); sig = sig.replace(/^Signature /, '');
req.setHeader('Signature', sig); req.setHeader('Signature', sig);
req.on('timeout', () => req.abort());
req.on('error', e => {
if (req.aborted) reject('timeout');
reject(e);
});
req.end(data); req.end(data);
}); });

View File

@ -7,6 +7,7 @@ const log = debug('misskey:activitypub:resolver');
export default class Resolver { export default class Resolver {
private history: Set<string>; private history: Set<string>;
private timeout = 10 * 1000;
constructor() { constructor() {
this.history = new Set(); this.history = new Set();
@ -19,11 +20,11 @@ export default class Resolver {
switch (collection.type) { switch (collection.type) {
case 'Collection': case 'Collection':
collection.objects = collection.object.items; collection.objects = collection.items;
break; break;
case 'OrderedCollection': case 'OrderedCollection':
collection.objects = collection.object.orderedItems; collection.objects = collection.orderedItems;
break; break;
default: default:
@ -50,6 +51,7 @@ export default class Resolver {
const object = await request({ const object = await request({
url: value, url: value,
timeout: this.timeout,
headers: { headers: {
'User-Agent': config.user_agent, 'User-Agent': config.user_agent,
Accept: 'application/activity+json, application/ld+json' Accept: 'application/activity+json, application/ld+json'

View File

@ -91,6 +91,14 @@ export interface IReject extends IActivity {
type: 'Reject'; type: 'Reject';
} }
export interface IAdd extends IActivity {
type: 'Add';
}
export interface IRemove extends IActivity {
type: 'Remove';
}
export interface ILike extends IActivity { export interface ILike extends IActivity {
type: 'Like'; type: 'Like';
_misskey_reaction: string; _misskey_reaction: string;
@ -109,5 +117,7 @@ export type Object =
IFollow | IFollow |
IAccept | IAccept |
IReject | IReject |
IAdd |
IRemove |
ILike | ILike |
IAnnounce; IAnnounce;

View File

@ -1,5 +1,5 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import DriveFile, { pack } from '../../../../models/drive-file'; import DriveFile, { packMany } from '../../../../models/drive-file';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
export const meta = { export const meta = {
@ -73,6 +73,5 @@ export default async (params: any, user: ILocalUser) => {
}); });
// Serialize // Serialize
const _files = await Promise.all(files.map(file => pack(file))); return await packMany(files);
return _files;
}; };

View File

@ -1,5 +1,5 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import DriveFile, { pack } from '../../../../models/drive-file'; import DriveFile, { packMany } from '../../../../models/drive-file';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
export const meta = { export const meta = {
@ -63,5 +63,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
}); });
// Serialize // Serialize
res(await Promise.all(files.map(file => pack(file)))); res(await packMany(files));
}); });

View File

@ -1,5 +1,5 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Favorite, { pack } from '../../../../models/favorite'; import Favorite, { packMany } from '../../../../models/favorite';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
export const meta = { export const meta = {
@ -55,5 +55,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
.find(query, { limit, sort }); .find(query, { limit, sort });
// Serialize // Serialize
res(await Promise.all(favorites.map(favorite => pack(favorite, user)))); res(await packMany(favorites, user));
}); });

View File

@ -1,7 +1,7 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Notification from '../../../../models/notification'; import Notification from '../../../../models/notification';
import Mute from '../../../../models/mute'; import Mute from '../../../../models/mute';
import { pack } from '../../../../models/notification'; import { packMany } from '../../../../models/notification';
import { getFriendIds } from '../../common/get-friends'; import { getFriendIds } from '../../common/get-friends';
import read from '../../common/read-notification'; import read from '../../common/read-notification';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
@ -83,7 +83,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
}); });
// Serialize // Serialize
res(await Promise.all(notifications.map(notification => pack(notification)))); res(await packMany(notifications));
// Mark all as read // Mark all as read
if (notifications.length > 0 && markAsRead) { if (notifications.length > 0 && markAsRead) {

View File

@ -1,8 +1,7 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import User, { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
import Note from '../../../../models/note';
import { pack } from '../../../../models/user'; import { pack } from '../../../../models/user';
import { deliverPinnedChange } from '../../../../services/i/pin'; import { addPinned } from '../../../../services/i/pin';
import getParams from '../../get-params'; import getParams from '../../get-params';
export const meta = { export const meta = {
@ -27,41 +26,18 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
const [ps, psErr] = getParams(meta, params); const [ps, psErr] = getParams(meta, params);
if (psErr) return rej(psErr); if (psErr) return rej(psErr);
// Fetch pinee // Processing
const note = await Note.findOne({ try {
_id: ps.noteId, await addPinned(user, ps.noteId);
userId: user._id } catch (e) {
}); return rej(e.message);
if (note === null) {
return rej('note not found');
} }
const pinnedNoteIds = user.pinnedNoteIds || []; // Serialize
if (pinnedNoteIds.length > 5) {
return rej('cannot pin more notes');
}
if (pinnedNoteIds.some(id => id.equals(note._id))) {
return rej('already exists');
}
pinnedNoteIds.unshift(note._id);
await User.update(user._id, {
$set: {
pinnedNoteIds: pinnedNoteIds
}
});
const iObj = await pack(user, user, { const iObj = await pack(user, user, {
detail: true detail: true
}); });
// Send response // Send response
res(iObj); res(iObj);
// Send Add to followers
deliverPinnedChange(user._id, note._id, true);
}); });

View File

@ -1,8 +1,7 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import User, { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
import Note from '../../../../models/note';
import { pack } from '../../../../models/user'; import { pack } from '../../../../models/user';
import { deliverPinnedChange } from '../../../../services/i/pin'; import { removePinned } from '../../../../services/i/pin';
import getParams from '../../get-params'; import getParams from '../../get-params';
export const meta = { export const meta = {
@ -27,31 +26,18 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
const [ps, psErr] = getParams(meta, params); const [ps, psErr] = getParams(meta, params);
if (psErr) return rej(psErr); if (psErr) return rej(psErr);
// Fetch unpinee // Processing
const note = await Note.findOne({ try {
_id: ps.noteId, await removePinned(user, ps.noteId);
userId: user._id } catch (e) {
}); return rej(e.message);
if (note === null) {
return rej('note not found');
} }
const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id)); // Serialize
await User.update(user._id, {
$set: {
pinnedNoteIds: pinnedNoteIds
}
});
const iObj = await pack(user, user, { const iObj = await pack(user, user, {
detail: true detail: true
}); });
// Send response // Send response
res(iObj); res(iObj);
// Send Remove to followers
deliverPinnedChange(user._id, note._id, false);
}); });

View File

@ -39,6 +39,15 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null, recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null,
swPublickey: config.sw ? config.sw.public_key : null, swPublickey: config.sw ? config.sw.public_key : null,
hidedTags: (me && me.isAdmin) ? meta.hidedTags : undefined, hidedTags: (me && me.isAdmin) ? meta.hidedTags : undefined,
bannerUrl: meta.bannerUrl bannerUrl: meta.bannerUrl,
features: {
registration: !meta.disableRegistration,
localTimeLine: !meta.disableLocalTimeline,
elasticsearch: config.elasticsearch ? true : false,
recaptcha: config.recaptcha ? true : false,
objectStorage: config.drive && config.drive.storage === 'minio',
twitter: config.twitter ? true : false,
serviceWorker: config.sw ? true : false
}
}); });
}); });

View File

@ -1,5 +1,5 @@
import $ from 'cafy'; import ID from '../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../misc/cafy-id';
import Note, { pack } from '../../../models/note'; import Note, { packMany } from '../../../models/note';
import getParams from '../get-params'; import getParams from '../get-params';
export const meta = { export const meta = {
@ -116,5 +116,5 @@ export default (params: any) => new Promise(async (res, rej) => {
}); });
// Serialize // Serialize
res(await Promise.all(notes.map(note => pack(note)))); res(await packMany(notes));
}); });

View File

@ -1,5 +1,5 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Note, { pack, INote } from '../../../../models/note'; import Note, { packMany, INote } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
/** /**
@ -52,5 +52,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
} }
// Serialize // Serialize
res(await Promise.all(conversation.map(note => pack(note, user)))); res(await packMany(conversation, user));
}); });

View File

@ -1,7 +1,7 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Note from '../../../../models/note'; import Note from '../../../../models/note';
import Mute from '../../../../models/mute'; import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note'; import { packMany } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params'; import getParams from '../../get-params';
import { countIf } from '../../../../prelude/array'; import { countIf } from '../../../../prelude/array';
@ -113,5 +113,5 @@ export default async (params: any, user: ILocalUser) => {
}); });
// Serialize // Serialize
return await Promise.all(timeline.map(note => pack(note, user))); return await packMany(timeline, user);
}; };

View File

@ -2,7 +2,7 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Note from '../../../../models/note'; import Note from '../../../../models/note';
import Mute from '../../../../models/mute'; import Mute from '../../../../models/mute';
import { getFriends } from '../../common/get-friends'; import { getFriends } from '../../common/get-friends';
import { pack } from '../../../../models/note'; import { packMany } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params'; import getParams from '../../get-params';
import { countIf } from '../../../../prelude/array'; import { countIf } from '../../../../prelude/array';
@ -240,5 +240,5 @@ export default async (params: any, user: ILocalUser) => {
}); });
// Serialize // Serialize
return await Promise.all(timeline.map(note => pack(note, user))); return await packMany(timeline, user);
}; };

View File

@ -1,7 +1,7 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Note from '../../../../models/note'; import Note from '../../../../models/note';
import Mute from '../../../../models/mute'; import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note'; import { packMany } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params'; import getParams from '../../get-params';
import { countIf } from '../../../../prelude/array'; import { countIf } from '../../../../prelude/array';
@ -141,5 +141,5 @@ export default async (params: any, user: ILocalUser) => {
}); });
// Serialize // Serialize
return await Promise.all(timeline.map(note => pack(note, user))); return await packMany(timeline, user);
}; };

View File

@ -1,7 +1,7 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Note from '../../../../models/note'; import Note from '../../../../models/note';
import { getFriendIds } from '../../common/get-friends'; import { getFriendIds } from '../../common/get-friends';
import { pack } from '../../../../models/note'; import { packMany } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params'; import getParams from '../../get-params';
import read from '../../../../services/note/read'; import read from '../../../../services/note/read';
@ -89,5 +89,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
mentions.forEach(note => read(user._id, note._id)); mentions.forEach(note => read(user._id, note._id));
// Serialize // Serialize
res(await Promise.all(mentions.map(mention => pack(mention, user)))); res(await packMany(mentions, user));
}); });

View File

@ -1,5 +1,5 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Note, { pack } from '../../../../models/note'; import Note, { packMany } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
/** /**
@ -30,5 +30,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
const ids = (note._replyIds || []).slice(offset, offset + limit); const ids = (note._replyIds || []).slice(offset, offset + limit);
// Serialize // Serialize
res(await Promise.all(ids.map(id => pack(id, user)))); res(await packMany(ids, user));
}); });

View File

@ -1,5 +1,5 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Note, { pack } from '../../../../models/note'; import Note, { packMany } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
/** /**
@ -62,6 +62,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
}); });
// Serialize // Serialize
res(await Promise.all(renotes.map(async note => res(await packMany(renotes, user));
await pack(note, user))));
}); });

View File

@ -2,7 +2,7 @@ import $ from 'cafy';
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import Note from '../../../../models/note'; import Note from '../../../../models/note';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
import { pack } from '../../../../models/note'; import { packMany } from '../../../../models/note';
import es from '../../../../db/elasticsearch'; import es from '../../../../db/elasticsearch';
export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
@ -60,6 +60,6 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
} }
}); });
res(await Promise.all(notes.map(note => pack(note, me)))); res(await packMany(notes, me));
}); });
}); });

View File

@ -3,7 +3,7 @@ import Note from '../../../../models/note';
import User, { ILocalUser } from '../../../../models/user'; import User, { ILocalUser } from '../../../../models/user';
import Mute from '../../../../models/mute'; import Mute from '../../../../models/mute';
import { getFriendIds } from '../../common/get-friends'; import { getFriendIds } from '../../common/get-friends';
import { pack } from '../../../../models/note'; import { packMany } from '../../../../models/note';
import getParams from '../../get-params'; import getParams from '../../get-params';
import { erase } from '../../../../prelude/array'; import { erase } from '../../../../prelude/array';
@ -363,5 +363,5 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}); });
// Serialize // Serialize
res(await Promise.all(notes.map(note => pack(note, me)))); res(await packMany(notes, me));
}); });

View File

@ -2,7 +2,7 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Note from '../../../../models/note'; import Note from '../../../../models/note';
import Mute from '../../../../models/mute'; import Mute from '../../../../models/mute';
import { getFriends } from '../../common/get-friends'; import { getFriends } from '../../common/get-friends';
import { pack } from '../../../../models/note'; import { packMany } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params'; import getParams from '../../get-params';
import { countIf } from '../../../../prelude/array'; import { countIf } from '../../../../prelude/array';
@ -237,5 +237,5 @@ export default async (params: any, user: ILocalUser) => {
}); });
// Serialize // Serialize
return await Promise.all(timeline.map(note => pack(note, user))); return await packMany(timeline, user);
}; };

View File

@ -1,7 +1,7 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Note from '../../../../models/note'; import Note from '../../../../models/note';
import Mute from '../../../../models/mute'; import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note'; import { packMany } from '../../../../models/note';
import UserList from '../../../../models/user-list'; import UserList from '../../../../models/user-list';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params'; import getParams from '../../get-params';
@ -242,5 +242,5 @@ export default async (params: any, user: ILocalUser) => {
}); });
// Serialize // Serialize
return await Promise.all(timeline.map(note => pack(note, user))); return await packMany(timeline, user);
}; };

View File

@ -1,6 +1,6 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import getHostLower from '../../common/get-host-lower'; import getHostLower from '../../common/get-host-lower';
import Note, { pack } from '../../../../models/note'; import Note, { packMany } from '../../../../models/note';
import User, { ILocalUser } from '../../../../models/user'; import User, { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params'; import getParams from '../../get-params';
import { countIf } from '../../../../prelude/array'; import { countIf } from '../../../../prelude/array';
@ -181,5 +181,5 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}); });
// Serialize // Serialize
res(await Promise.all(notes.map(note => pack(note, me)))); res(await packMany(notes, me));
}); });

View File

@ -1,12 +1,83 @@
import config from '../../config'; import config from '../../config';
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import User, { isLocalUser, isRemoteUser, ILocalUser } from '../../models/user'; import User, { isLocalUser, isRemoteUser, ILocalUser, IUser } from '../../models/user';
import Note from '../../models/note';
import Following from '../../models/following'; import Following from '../../models/following';
import renderAdd from '../../remote/activitypub/renderer/add'; import renderAdd from '../../remote/activitypub/renderer/add';
import renderRemove from '../../remote/activitypub/renderer/remove'; import renderRemove from '../../remote/activitypub/renderer/remove';
import packAp from '../../remote/activitypub/renderer'; import packAp from '../../remote/activitypub/renderer';
import { deliver } from '../../queue'; import { deliver } from '../../queue';
/**
* 指定した投稿をピン留めします
* @param user
* @param noteId
*/
export async function addPinned(user: IUser, noteId: mongo.ObjectID) {
// Fetch pinee
const note = await Note.findOne({
_id: noteId,
userId: user._id
});
if (note === null) {
throw new Error('note not found');
}
const pinnedNoteIds = user.pinnedNoteIds || [];
if (pinnedNoteIds.length >= 5) {
throw new Error('cannot pin more notes');
}
if (pinnedNoteIds.some(id => id.equals(note._id))) {
throw new Error('already exists');
}
pinnedNoteIds.unshift(note._id);
await User.update(user._id, {
$set: {
pinnedNoteIds: pinnedNoteIds
}
});
// Deliver to remote followers
if (isLocalUser(user)) {
deliverPinnedChange(user._id, note._id, true);
}
}
/**
* 指定した投稿のピン留めを解除します
* @param user
* @param noteId
*/
export async function removePinned(user: IUser, noteId: mongo.ObjectID) {
// Fetch unpinee
const note = await Note.findOne({
_id: noteId,
userId: user._id
});
if (note === null) {
throw new Error('note not found');
}
const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id));
await User.update(user._id, {
$set: {
pinnedNoteIds: pinnedNoteIds
}
});
// Deliver to remote followers
if (isLocalUser(user)) {
deliverPinnedChange(user._id, noteId, false);
}
}
export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.ObjectID, isAddition: boolean) { export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.ObjectID, isAddition: boolean) {
const user = await User.findOne({ const user = await User.findOne({
_id: userId _id: userId

View File

@ -196,6 +196,9 @@ module.exports = {
}, { }, {
test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/, test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/,
loader: 'url-loader' loader: 'url-loader'
}, {
test: /\.json5$/,
loader: 'json5-loader'
}, { }, {
test: /\.ts$/, test: /\.ts$/,
exclude: /node_modules/, exclude: /node_modules/,