summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml11
-rw-r--r--appveyor.yml41
-rw-r--r--gulpfile.ts2
-rw-r--r--locales/en.yml4
-rw-r--r--locales/index.ts1
-rw-r--r--locales/ja.yml49
-rw-r--r--package.json75
-rw-r--r--src/build/fa.ts5
-rw-r--r--src/client/app/boot.js2
-rw-r--r--src/client/app/common/scripts/can-hide-text.ts16
-rw-r--r--src/client/app/common/scripts/streaming/home.ts11
-rw-r--r--src/client/app/common/views/components/acct.vue19
-rw-r--r--src/client/app/common/views/components/avatar.vue2
-rw-r--r--src/client/app/common/views/components/index.ts2
-rw-r--r--src/client/app/common/views/components/url-preview.vue17
-rw-r--r--src/client/app/common/views/widgets/calendar.vue185
-rw-r--r--src/client/app/common/views/widgets/rss.vue10
-rw-r--r--src/client/app/desktop/views/components/drive.file.vue4
-rw-r--r--src/client/app/desktop/views/components/home.vue48
-rw-r--r--src/client/app/desktop/views/components/media-image.vue2
-rw-r--r--src/client/app/desktop/views/components/note-detail.sub.vue2
-rw-r--r--src/client/app/desktop/views/components/note-detail.vue2
-rw-r--r--src/client/app/desktop/views/components/note-preview.vue7
-rw-r--r--src/client/app/desktop/views/components/notes.note.sub.vue2
-rw-r--r--src/client/app/desktop/views/components/notes.note.vue21
-rw-r--r--src/client/app/desktop/views/components/settings.api.vue2
-rw-r--r--src/client/app/desktop/views/components/settings.vue11
-rw-r--r--src/client/app/desktop/views/components/user-lists-window.vue2
-rw-r--r--src/client/app/desktop/views/components/user-preview.vue6
-rw-r--r--src/client/app/desktop/views/components/users-list.vue10
-rw-r--r--src/client/app/desktop/views/pages/user/user.header.vue2
-rw-r--r--src/client/app/mobile/api/post.ts49
-rw-r--r--src/client/app/mobile/script.ts2
-rw-r--r--src/client/app/mobile/views/components/drive.file-detail.vue2
-rw-r--r--src/client/app/mobile/views/components/drive.file.vue2
-rw-r--r--src/client/app/mobile/views/components/media-image.vue2
-rw-r--r--src/client/app/mobile/views/components/note-preview.vue3
-rw-r--r--src/client/app/mobile/views/components/note.vue16
-rw-r--r--src/client/app/mobile/views/components/notes.vue19
-rw-r--r--src/client/app/mobile/views/components/post-form.vue12
-rw-r--r--src/client/app/mobile/views/components/ui.header.vue5
-rw-r--r--src/client/app/mobile/views/components/ui.nav.vue1
-rw-r--r--src/client/app/mobile/views/components/widget-container.vue22
-rw-r--r--src/client/app/mobile/views/pages/user.vue2
-rw-r--r--src/client/app/mobile/views/pages/widgets.vue (renamed from src/client/app/mobile/views/pages/dashboard.vue)69
-rw-r--r--src/client/app/store.ts41
-rw-r--r--src/client/docs/api/gulpfile.ts4
-rw-r--r--src/client/docs/gulpfile.ts2
-rw-r--r--src/models/drive-file.ts1
-rw-r--r--src/server/web/url-preview.ts18
-rw-r--r--src/services/drive/add-file.ts5
-rw-r--r--test/api.ts1152
-rw-r--r--test/text.ts10
-rw-r--r--webpack.config.ts39
-rw-r--r--webpack/loaders/replace.js36
55 files changed, 568 insertions, 1519 deletions
diff --git a/.travis.yml b/.travis.yml
index d2552bb460..c86b737d21 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,7 +7,7 @@ notifications:
language: node_js
node_js:
- - 9.8.0
+ - 10.1.0
env:
- CXX=g++-4.8 NODE_ENV=production
@@ -22,19 +22,14 @@ addons:
cache:
directories:
- # パッケージをキャッシュすると本来は動かないはずなのに動いてしまう
- # 場合があり危険なのでキャッシュはしない:
- #- node_modules
+ - node_modules
services:
- mongodb
- redis-server
before_script:
- # Travisはproduction環境なので(10行目により)、
- # npm install しただけでは devDependencies はインストールされないので、
- # --only=dev オプションを付けてそれらもインストールされるようにする:
- - npm install --only=dev
+ - npm install
# 設定ファイルを配置
- cp ./.travis/default.yml ./.config
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000000..280d6a99ae
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,41 @@
+# appveyor file
+# http://www.appveyor.com/docs/appveyor-yml
+
+environment:
+ matrix:
+ - nodejs_version: 10.1.0
+
+cache:
+ - node_modules
+
+build: off
+
+install:
+ # Update Node.js
+ # 標準で入っている Node.js を更新します (2014/11/13 時点では、v0.10.32 が標準)
+ - ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version)
+ - node --version
+
+ # Update NPM
+ - npm install -g npm
+ - npm --version
+
+ # Update node-gyp
+ # 必須! node-gyp のバージョンを上げないと、ネイティブモジュールのコンパイルに失敗します
+ - npm install -g node-gyp
+
+ - npm install
+
+init:
+ # git clone の際の改行を変換しないようにします
+ - git config --global core.autocrlf false
+
+before_test:
+ # 設定ファイルを配置
+ - cp ./.travis/default.yml ./.config
+ - cp ./.travis/test.yml ./.config
+
+ - npm run build
+
+test_script:
+ - npm test
diff --git a/gulpfile.ts b/gulpfile.ts
index a9ccbbdb5e..fa1155878c 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -20,6 +20,7 @@ import * as replace from 'gulp-replace';
import * as htmlmin from 'gulp-htmlmin';
const uglifyes = require('uglify-es');
+import locales from './locales';
import { fa } from './src/build/fa';
const client = require('./built/client/meta.json');
import config from './src/config';
@@ -122,6 +123,7 @@ gulp.task('build:client:script', () =>
.pipe(replace('VERSION', JSON.stringify(client.version)))
.pipe(replace('API', JSON.stringify(config.api_url)))
.pipe(replace('ENV', JSON.stringify(env)))
+ .pipe(replace('LANGS', JSON.stringify(Object.keys(locales))))
.pipe(isProduction ? uglify({
toplevel: true
} as any) : gutil.noop())
diff --git a/locales/en.yml b/locales/en.yml
index da290d8427..9f2a0be3ae 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -1,7 +1,7 @@
---
meta:
- lang: "日本語"
- divider: ""
+ lang: "English"
+ divider: " "
common:
misskey: "Share everything with others using Misskey."
time:
diff --git a/locales/index.ts b/locales/index.ts
index 89d18190f6..319d178e0a 100644
--- a/locales/index.ts
+++ b/locales/index.ts
@@ -11,6 +11,7 @@ const loadLang = lang => yaml.safeLoad(
const native = loadLang('ja');
const langs = {
+ 'de': loadLang('de'),
'en': loadLang('en'),
'fr': loadLang('fr'),
'ja': native,
diff --git a/locales/ja.yml b/locales/ja.yml
index d8e15fb3c0..d71251d203 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -1,3 +1,7 @@
+meta:
+ lang: "日本語"
+ divider: ""
+
common:
misskey: "Misskeyで皆と共有しよう。"
@@ -253,6 +257,32 @@ desktop/views/components/drive.vue:
upload: "ファイルをアップロード"
url-upload: "URLからアップロード"
+desktop/views/components/home.vue:
+ done: "完了"
+ add-widget: "ウィジェットを追加:"
+ profile: "プロフィール"
+ calendar: "カレンダー"
+ timemachine: "カレンダー(タイムマシン)"
+ activity: "アクティビティ"
+ rss: "RSSリーダー"
+ trends: "トレンド"
+ photostream: "フォトストリーム"
+ slideshow: "スライドショー"
+ version: "バージョン"
+ broadcast: "ブロードキャスト"
+ notifications: "通知"
+ users: "おすすめユーザー"
+ polls: "投票"
+ post-form: "投稿フォーム"
+ messaging: "メッセージ"
+ channel: "チャンネル"
+ access-log: "アクセスログ"
+ server: "サーバー情報"
+ donation: "寄付のお願い"
+ nav: "ナビゲーション"
+ tips: "ヒント"
+ add: "追加"
+
desktop/views/components/messaging-window.vue:
title: "メッセージ"
@@ -312,6 +342,7 @@ desktop/views/components/settings.vue:
mute: "ミュート"
drive: "ドライブ"
security: "セキュリティ"
+ signin: "サインイン履歴"
password: "パスワード"
2fa: "二段階認証"
other: "その他"
@@ -341,6 +372,7 @@ desktop/views/components/settings.api.vue:
caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。"
regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。"
regenerate-token: "トークンを再生成"
+ token: "Token:"
enter-password: "パスワードを入力してください"
desktop/views/components/settings.app.vue:
@@ -396,6 +428,20 @@ desktop/views/components/ui.header.post.vue:
desktop/views/components/ui.header.search.vue:
placeholder: "検索"
+desktop/views/components/user-lists-window.vue:
+ create-list: "リストを作成"
+
+desktop/views/components/user-preview.vue:
+ notes: "投稿"
+ following: "フォロー"
+ followers: "フォロワー"
+
+desktop/views/components/users-list.vue:
+ all: "すべて"
+ iknow: "知り合い"
+ load-more: "もっと"
+ fetching: "読み込んでいます"
+
desktop/views/pages/note.vue:
prev: "前の投稿"
next: "次の投稿"
@@ -510,6 +556,9 @@ mobile/views/components/notifications.vue:
mobile/views/components/post-form.vue:
submit: "投稿"
+ reply: "返信"
+ renote: "Renote"
+ renote-placeholder: "この投稿を引用... (オプション)"
reply-placeholder: "この投稿への返信..."
note-placeholder: "いまどうしてる?"
diff --git a/package.json b/package.json
index 544dda6036..6b7f63d6c8 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
- "version": "2.6.2",
- "clientVersion": "1.0.5260",
+ "version": "2.10.0",
+ "clientVersion": "1.0.5406",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,
@@ -31,13 +31,11 @@
"@prezzemolo/rap": "0.1.2",
"@prezzemolo/zip": "0.0.3",
"@types/bcryptjs": "2.4.1",
- "@types/chai": "4.1.3",
- "@types/chai-http": "3.0.4",
"@types/debug": "0.0.30",
"@types/deep-equal": "1.0.1",
- "@types/elasticsearch": "5.0.22",
+ "@types/elasticsearch": "5.0.23",
"@types/eventemitter3": "2.0.2",
- "@types/gm": "1.17.33",
+ "@types/gm": "1.18.0",
"@types/gulp": "3.8.36",
"@types/gulp-htmlmin": "1.3.32",
"@types/gulp-mocha": "0.0.32",
@@ -58,18 +56,18 @@
"@types/koa-multer": "1.0.0",
"@types/koa-router": "7.0.28",
"@types/koa-send": "4.1.1",
- "@types/koa-views": "^2.0.3",
+ "@types/koa-views": "2.0.3",
"@types/koa__cors": "2.2.2",
"@types/kue": "0.11.8",
"@types/license-checker": "15.0.0",
"@types/mkdirp": "0.5.2",
"@types/mocha": "5.2.0",
- "@types/mongodb": "3.0.15",
+ "@types/mongodb": "3.0.18",
"@types/monk": "6.0.0",
"@types/ms": "0.7.30",
- "@types/node": "9.6.6",
+ "@types/node": "10.1.0",
"@types/nopt": "3.0.29",
- "@types/parse5": "^3.0.0",
+ "@types/parse5": "3.0.0",
"@types/pug": "2.0.4",
"@types/qrcode": "0.8.1",
"@types/ratelimiter": "2.1.28",
@@ -78,22 +76,20 @@
"@types/request-promise-native": "1.0.14",
"@types/rimraf": "2.0.2",
"@types/seedrandom": "2.4.27",
- "@types/single-line-log": "^1.1.0",
+ "@types/single-line-log": "1.1.0",
"@types/speakeasy": "2.0.2",
"@types/tmp": "0.0.33",
"@types/uuid": "3.4.3",
- "@types/webpack": "4.1.4",
+ "@types/webpack": "4.1.7",
"@types/webpack-stream": "3.2.10",
- "@types/websocket": "0.0.38",
- "@types/ws": "4.0.2",
+ "@types/websocket": "0.0.39",
+ "@types/ws": "5.1.1",
"animejs": "2.2.0",
- "autosize": "4.0.1",
+ "autosize": "4.0.2",
"autwh": "0.1.0",
"bcryptjs": "2.4.3",
"bootstrap-vue": "2.0.0-rc.6",
"cafy": "8.0.0",
- "chai": "4.1.2",
- "chai-http": "4.0.0",
"chalk": "2.4.1",
"crc-32": "1.2.0",
"css-loader": "0.28.11",
@@ -101,9 +97,9 @@
"deep-equal": "1.0.1",
"deepcopy": "0.6.3",
"diskusage": "0.2.4",
- "dompurify": "1.0.3",
+ "dompurify": "1.0.4",
"elasticsearch": "14.2.2",
- "element-ui": "2.3.6",
+ "element-ui": "2.3.8",
"emojilib": "2.2.12",
"escape-regexp": "0.0.1",
"eslint": "4.19.1",
@@ -111,7 +107,7 @@
"eventemitter3": "3.1.0",
"exif-js": "2.3.0",
"file-loader": "1.1.11",
- "file-type": "7.6.0",
+ "file-type": "8.0.0",
"fuckadblock": "3.2.1",
"gm": "1.23.1",
"gulp": "3.9.1",
@@ -120,15 +116,15 @@
"gulp-imagemin": "4.1.0",
"gulp-mocha": "5.0.0",
"gulp-pug": "4.0.1",
- "gulp-rename": "1.2.2",
- "gulp-replace": "0.6.1",
+ "gulp-rename": "1.2.3",
+ "gulp-replace": "1.0.0",
"gulp-sourcemaps": "2.6.4",
"gulp-stylus": "2.7.0",
"gulp-tslint": "8.1.3",
"gulp-typescript": "4.0.2",
"gulp-uglify": "3.0.0",
"gulp-util": "3.0.8",
- "hard-source-webpack-plugin": "0.6.4",
+ "hard-source-webpack-plugin": "0.6.7",
"highlight.js": "9.12.0",
"html-minifier": "3.5.15",
"http-signature": "1.2.0",
@@ -136,7 +132,7 @@
"is-root": "2.0.0",
"is-url": "1.2.4",
"js-yaml": "3.11.0",
- "jsdom": "11.9.0",
+ "jsdom": "11.10.0",
"koa": "2.5.1",
"koa-bodyparser": "4.2.0",
"koa-compress": "3.0.0",
@@ -148,16 +144,16 @@
"koa-router": "7.4.0",
"koa-send": "4.1.3",
"koa-slow": "2.1.0",
- "koa-views": "^6.1.4",
+ "koa-views": "6.1.4",
"kue": "0.11.6",
- "license-checker": "18.0.0",
+ "license-checker": "19.0.0",
"loader-utils": "1.1.0",
"mecab-async": "0.1.2",
"mkdirp": "0.5.1",
"mocha": "5.1.1",
"moji": "0.5.1",
- "mongodb": "3.0.7",
- "monk": "6.0.5",
+ "mongodb": "3.0.8",
+ "monk": "6.0.6",
"ms": "2.1.1",
"nan": "2.10.0",
"node-sass": "4.9.0",
@@ -167,10 +163,10 @@
"object-assign-deep": "0.4.0",
"on-build-webpack": "0.1.0",
"os-utils": "0.0.14",
- "parse5": "^4.0.0",
+ "parse5": "4.0.0",
"progress-bar-webpack-plugin": "1.11.0",
"prominence": "0.2.0",
- "promise-sequential": "^1.1.1",
+ "promise-sequential": "1.1.1",
"pug": "2.0.3",
"punycode": "2.1.0",
"qrcode": "1.2.0",
@@ -178,14 +174,14 @@
"recaptcha-promise": "0.1.3",
"reconnecting-websocket": "3.2.2",
"redis": "2.8.0",
- "request": "2.85.0",
+ "request": "2.86.0",
"request-promise-native": "1.0.5",
"rimraf": "2.6.2",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sass-loader": "7.0.1",
"seedrandom": "2.4.3",
- "single-line-log": "^1.1.2",
+ "single-line-log": "1.1.2",
"speakeasy": "2.0.0",
"style-loader": "0.21.0",
"stylus": "0.54.5",
@@ -196,9 +192,9 @@
"tcp-port-used": "0.1.2",
"textarea-caret": "3.1.0",
"tmp": "0.0.33",
- "ts-loader": "4.2.0",
- "ts-node": "6.0.1",
- "tslint": "5.9.1",
+ "ts-loader": "4.3.0",
+ "ts-node": "6.0.3",
+ "tslint": "5.10.0",
"typescript": "2.8.3",
"typescript-eslint-parser": "15.0.0",
"uglify-es": "3.3.9",
@@ -209,16 +205,15 @@
"vue-cropperjs": "2.2.0",
"vue-js-modal": "1.3.13",
"vue-json-tree-view": "2.1.4",
- "vue-loader": "15.0.3",
+ "vue-loader": "15.0.11",
"vue-router": "3.0.1",
"vue-template-compiler": "2.5.16",
"vuedraggable": "2.16.0",
"vuex": "3.0.1",
- "web-push": "3.3.0",
+ "web-push": "3.3.1",
"webfinger.js": "2.6.6",
- "webpack": "4.6.0",
- "webpack-cli": "2.0.15",
- "webpack-replace-loader": "1.3.0",
+ "webpack": "4.8.3",
+ "webpack-cli": "2.1.3",
"websocket": "1.0.26",
"ws": "5.1.1",
"xev": "2.0.0"
diff --git a/src/build/fa.ts b/src/build/fa.ts
index f6f2427d0a..111c19ae66 100644
--- a/src/build/fa.ts
+++ b/src/build/fa.ts
@@ -7,10 +7,7 @@ import * as regular from '@fortawesome/fontawesome-free-regular';
import * as solid from '@fortawesome/fontawesome-free-solid';
import * as brands from '@fortawesome/fontawesome-free-brands';
-// Add icons
-fontawesome.library.add(regular);
-fontawesome.library.add(solid);
-fontawesome.library.add(brands);
+fontawesome.library.add(regular, solid, brands);
export const pattern = /%fa:(.+?)%/g;
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 35d02cf9c5..9338bc501e 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -32,7 +32,7 @@
// Detect the user language
// Note: The default language is Japanese
let lang = navigator.language.split('-')[0];
- if (!/^(en|ja)$/.test(lang)) lang = 'ja';
+ if (!LANGS.includes(lang)) lang = 'en';
if (localStorage.getItem('lang')) lang = localStorage.getItem('lang');
// Detect the user agent
diff --git a/src/client/app/common/scripts/can-hide-text.ts b/src/client/app/common/scripts/can-hide-text.ts
new file mode 100644
index 0000000000..4a4be8d9d0
--- /dev/null
+++ b/src/client/app/common/scripts/can-hide-text.ts
@@ -0,0 +1,16 @@
+export default function(note) {
+ if (note.text == null) return true;
+
+ let txt = note.text;
+
+ if (note.media) {
+ note.media.forEach(file => {
+ txt = txt.replace(file.url, '');
+ if (file.src) txt = txt.replace(file.src, '');
+ });
+
+ if (txt == '') return true;
+ }
+
+ return false;
+}
diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
index 32685f3c2c..09d830bece 100644
--- a/src/client/app/common/scripts/streaming/home.ts
+++ b/src/client/app/common/scripts/streaming/home.ts
@@ -48,6 +48,17 @@ export class HomeStream extends Stream {
}
});
+ this.on('mobile_home_updated', x => {
+ if (x.home) {
+ os.store.commit('settings/setMobileHome', x.home);
+ } else {
+ os.store.commit('settings/setMobileHomeWidget', {
+ id: x.id,
+ data: x.data
+ });
+ }
+ });
+
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
this.on('my_token_regenerated', () => {
diff --git a/src/client/app/common/views/components/acct.vue b/src/client/app/common/views/components/acct.vue
new file mode 100644
index 0000000000..1ad222afdd
--- /dev/null
+++ b/src/client/app/common/views/components/acct.vue
@@ -0,0 +1,19 @@
+<template>
+<span class="mk-acct">
+ <span class="name">@{{ user.username }}</span>
+ <span class="host" v-if="user.host">@{{ user.host }}</span>
+</span>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+ props: ['user']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-acct
+ > .host
+ opacity 0.5
+</style>
diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
index a4648c272e..8ec359e83c 100644
--- a/src/client/app/common/views/components/avatar.vue
+++ b/src/client/app/common/views/components/avatar.vue
@@ -23,7 +23,7 @@ export default Vue.extend({
computed: {
style(): any {
return {
- backgroundColor: this.user.avatarColor ? `rgb(${ this.user.avatarColor.join(',') })` : null,
+ backgroundColor: this.user.avatarColor && this.user.avatarColor.length == 3 ? `rgb(${ this.user.avatarColor.join(',') })` : null,
backgroundImage: `url(${ this.user.avatarUrl }?thumbnail)`,
borderRadius: (this as any).clientSettings.circleIcons ? '100%' : null
};
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 69fed00c74..c1a7bc61d7 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -3,6 +3,7 @@ import Vue from 'vue';
import signin from './signin.vue';
import signup from './signup.vue';
import forkit from './forkit.vue';
+import acct from './acct.vue';
import avatar from './avatar.vue';
import nav from './nav.vue';
import noteHtml from './note-html';
@@ -29,6 +30,7 @@ import welcomeTimeline from './welcome-timeline.vue';
Vue.component('mk-signin', signin);
Vue.component('mk-signup', signup);
Vue.component('mk-forkit', forkit);
+Vue.component('mk-acct', acct);
Vue.component('mk-avatar', avatar);
Vue.component('mk-nav', nav);
Vue.component('mk-note-html', noteHtml);
diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue
index 3bae6e5078..028b911e24 100644
--- a/src/client/app/common/views/components/url-preview.vue
+++ b/src/client/app/common/views/components/url-preview.vue
@@ -126,16 +126,21 @@ root(isDark)
line-height 16px
vertical-align top
+ @media (max-width 700px)
+ > .thumbnail
+ position relative
+ width 100%
+ height 100px
+
+ & + article
+ left 0
+ width 100%
+
@media (max-width 500px)
font-size 8px
- border none
> .thumbnail
- width 70px
-
- & + article
- left 70px
- width calc(100% - 70px)
+ height 70px
> article
padding 8px
diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue
index 41e9253784..0e9714960a 100644
--- a/src/client/app/common/views/widgets/calendar.vue
+++ b/src/client/app/common/views/widgets/calendar.vue
@@ -1,37 +1,37 @@
<template>
-<div class="mkw-calendar"
- :data-melt="props.design == 1"
- :data-special="special"
- :data-mobile="isMobile"
->
- <div class="calendar" :data-is-holiday="isHoliday">
- <p class="month-and-year">
- <span class="year">{{ year }}年</span>
- <span class="month">{{ month }}月</span>
- </p>
- <p class="day">{{ day }}日</p>
- <p class="week-day">{{ weekDay }}曜日</p>
- </div>
- <div class="info">
- <div>
- <p>今日:<b>{{ dayP.toFixed(1) }}%</b></p>
- <div class="meter">
- <div class="val" :style="{ width: `${dayP}%` }"></div>
+<div class="mkw-calendar" :data-special="special" :data-mobile="isMobile">
+ <mk-widget-container :naked="props.design == 1" :show-header="false">
+ <div class="mkw-calendar--body">
+ <div class="calendar" :data-is-holiday="isHoliday">
+ <p class="month-and-year">
+ <span class="year">{{ year }}年</span>
+ <span class="month">{{ month }}月</span>
+ </p>
+ <p class="day">{{ day }}日</p>
+ <p class="week-day">{{ weekDay }}曜日</p>
</div>
- </div>
- <div>
- <p>今月:<b>{{ monthP.toFixed(1) }}%</b></p>
- <div class="meter">
- <div class="val" :style="{ width: `${monthP}%` }"></div>
- </div>
- </div>
- <div>
- <p>今年:<b>{{ yearP.toFixed(1) }}%</b></p>
- <div class="meter">
- <div class="val" :style="{ width: `${yearP}%` }"></div>
+ <div class="info">
+ <div>
+ <p>今日:<b>{{ dayP.toFixed(1) }}%</b></p>
+ <div class="meter">
+ <div class="val" :style="{ width: `${dayP}%` }"></div>
+ </div>
+ </div>
+ <div>
+ <p>今月:<b>{{ monthP.toFixed(1) }}%</b></p>
+ <div class="meter">
+ <div class="val" :style="{ width: `${monthP}%` }"></div>
+ </div>
+ </div>
+ <div>
+ <p>今年:<b>{{ yearP.toFixed(1) }}%</b></p>
+ <div class="meter">
+ <div class="val" :style="{ width: `${yearP}%` }"></div>
+ </div>
+ </div>
</div>
</div>
- </div>
+ </mk-widget-container>
</div>
</template>
@@ -111,93 +111,82 @@ export default define({
@import '~const.styl'
root(isDark)
- padding 16px 0
- color isDark ? #c5ced6 :#777
- background isDark ? #282C37 : #fff
- border solid 1px rgba(#000, 0.075)
- border-radius 6px
-
&[data-special='on-new-years-day']
border-color #ef95a0
- &[data-melt]
- background transparent
- border none
-
- &[data-mobile]
- border none
- border-radius 8px
- box-shadow 0 0 0 1px rgba(#000, 0.2)
+ .mkw-calendar--body
+ padding 16px 0
+ color isDark ? #c5ced6 : #777
- &:after
- content ""
- display block
- clear both
+ &:after
+ content ""
+ display block
+ clear both
- > .calendar
- float left
- width 60%
- text-align center
+ > .calendar
+ float left
+ width 60%
+ text-align center
- &[data-is-holiday]
- > .day
- color #ef95a0
+ &[data-is-holiday]
+ > .day
+ color #ef95a0
- > p
- margin 0
- line-height 18px
- font-size 14px
+ > p
+ margin 0
+ line-height 18px
+ font-size 14px
- > span
- margin 0 4px
+ > span
+ margin 0 4px
- > .day
- margin 10px 0
- line-height 32px
- font-size 28px
+ > .day
+ margin 10px 0
+ line-height 32px
+ font-size 28px
- > .info
- display block
- float left
- width 40%
- padding 0 16px 0 0
+ > .info
+ display block
+ float left
+ width 40%
+ padding 0 16px 0 0
- > div
- margin-bottom 8px
+ > div
+ margin-bottom 8px
- &:last-child
- margin-bottom 4px
+ &:last-child
+ margin-bottom 4px
- > p
- margin 0 0 2px 0
- font-size 12px
- line-height 18px
- color isDark ? #7a8692 : #888
+ > p
+ margin 0 0 2px 0
+ font-size 12px
+ line-height 18px
+ color isDark ? #7a8692 : #888
- > b
- margin-left 2px
+ > b
+ margin-left 2px
- > .meter
- width 100%
- overflow hidden
- background isDark ? #1c1f25 : #eee
- border-radius 8px
+ > .meter
+ width 100%
+ overflow hidden
+ background isDark ? #1c1f25 : #eee
+ border-radius 8px
- > .val
- height 4px
- background $theme-color
+ > .val
+ height 4px
+ background $theme-color
- &:nth-child(1)
- > .meter > .val
- background #f7796c
+ &:nth-child(1)
+ > .meter > .val
+ background #f7796c
- &:nth-child(2)
- > .meter > .val
- background #a1de41
+ &:nth-child(2)
+ > .meter > .val
+ background #a1de41
- &:nth-child(3)
- > .meter > .val
- background #41ddde
+ &:nth-child(3)
+ > .meter > .val
+ background #41ddde
.mkw-calendar[data-darkmode]
root(true)
diff --git a/src/client/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue
index b5339add0b..9e2c6b6490 100644
--- a/src/client/app/common/views/widgets/rss.vue
+++ b/src/client/app/common/views/widgets/rss.vue
@@ -1,10 +1,10 @@
<template>
-<div class="mkw-rss" :data-mobile="isMobile">
+<div class="mkw-rss">
<mk-widget-container :show-header="!props.compact">
<template slot="header">%fa:rss-square%RSS</template>
<button slot="func" title="設定" @click="setting">%fa:cog%</button>
- <div class="mkw-rss--body">
+ <div class="mkw-rss--body" :data-mobile="isMobile">
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
<div class="feed" v-else>
<a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a>
@@ -85,15 +85,17 @@ root(isDark)
margin-right 4px
&[data-mobile]
+ background isDark ? #21242f : #f3f3f3
+
.feed
padding 0
- font-size 1em
> a
padding 8px 16px
+ border-bottom none
&:nth-child(even)
- background rgba(#000, 0.05)
+ background isDark ? rgba(#000, 0.05) : rgba(#fff, 0.7)
.mkw-rss[data-darkmode]
root(true)
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index 39881711fa..d8b8420ece 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -50,7 +50,7 @@ export default Vue.extend({
return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`;
},
background(): string {
- return this.file.properties.avgColor
+ return this.file.properties.avgColor && this.file.properties.avgColor.length == 3
? `rgb(${this.file.properties.avgColor.join(',')})`
: 'transparent';
}
@@ -129,7 +129,7 @@ export default Vue.extend({
},
onThumbnailLoaded() {
- if (this.file.properties.avgColor) {
+ if (this.file.properties.avgColor && this.file.properties.avgColor.length == 3) {
anime({
targets: this.$refs.thumbnail,
backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`,
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index cae6233cd8..a3d7927cfc 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -1,34 +1,34 @@
<template>
<div class="mk-home" :data-customize="customize">
<div class="customize" v-if="customize">
- <router-link to="/">%fa:check%完了</router-link>
+ <router-link to="/">%fa:check%%i18n:@done%</router-link>
<div>
<div class="adder">
- <p>ウィジェットを追加:</p>
+ <p>%i18n:@add-widget%</p>
<select v-model="widgetAdderSelected">
- <option value="profile">プロフィール</option>
- <option value="calendar">カレンダー</option>
- <option value="timemachine">カレンダー(タイムマシン)</option>
- <option value="activity">アクティビティ</option>
- <option value="rss">RSSリーダー</option>
- <option value="trends">トレンド</option>
- <option value="photo-stream">フォトストリーム</option>
- <option value="slideshow">スライドショー</option>
- <option value="version">バージョン</option>
- <option value="broadcast">ブロードキャスト</option>
- <option value="notifications">通知</option>
- <option value="users">おすすめユーザー</option>
- <option value="polls">投票</option>
- <option value="post-form">投稿フォーム</option>
- <option value="messaging">メッセージ</option>
- <option value="channel">チャンネル</option>
- <option value="access-log">アクセスログ</option>
- <option value="server">サーバー情報</option>
- <option value="donation">寄付のお願い</option>
- <option value="nav">ナビゲーション</option>
- <option value="tips">ヒント</option>
+ <option value="profile">%i18n:@profile%</option>
+ <option value="calendar">%i18n:@calendar%</option>
+ <option value="timemachine">%i18n:@timemachine%</option>
+ <option value="activity">%i18n:@activity%</option>
+ <option value="rss">%i18n:@rss%</option>
+ <option value="trends">%i18n:@trends%</option>
+ <option value="photo-stream">%i18n:@photo-stream%</option>
+ <option value="slideshow">%i18n:@slideshow%</option>
+ <option value="version">%i18n:@version%</option>
+ <option value="broadcast">%i18n:@broadcast%</option>
+ <option value="notifications">%i18n:@notifications%</option>
+ <option value="users">%i18n:@users%</option>
+ <option value="polls">%i18n:@polls%</option>
+ <option value="post-form">%i18n:@post-form%</option>
+ <option value="messaging">%i18n:@messaging%</option>
+ <option value="channel">%i18n:@channel%</option>
+ <option value="access-log">%i18n:@access-log%</option>
+ <option value="server">%i18n:@server%</option>
+ <option value="donation">%i18n:@donation%</option>
+ <option value="nav">%i18n:@nav%</option>
+ <option value="tips">%i18n:@tips%</option>
</select>
- <button @click="addWidget">追加</button>
+ <button @click="addWidget">%i18n:@add%</button>
</div>
<div class="trash">
<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable>
diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue
index e5803cc36e..b98a4707ec 100644
--- a/src/client/app/desktop/views/components/media-image.vue
+++ b/src/client/app/desktop/views/components/media-image.vue
@@ -26,7 +26,7 @@ export default Vue.extend({
computed: {
style(): any {
return {
- 'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
+ 'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
};
}
diff --git a/src/client/app/desktop/views/components/note-detail.sub.vue b/src/client/app/desktop/views/components/note-detail.sub.vue
index 24550c4e94..32119da50d 100644
--- a/src/client/app/desktop/views/components/note-detail.sub.vue
+++ b/src/client/app/desktop/views/components/note-detail.sub.vue
@@ -5,7 +5,7 @@
<header>
<div class="left">
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
- <span class="username">@{{ note.user | acct }}</span>
+ <span class="username"><mk-acct :user="note.user"/></span>
</div>
<div class="right">
<router-link class="time" :to="note | notePage">
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index a0e3915149..bda53db918 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -28,7 +28,7 @@
<mk-avatar class="avatar" :user="p.user"/>
<header>
<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
- <span class="username">@{{ p.user | acct }}</span>
+ <span class="username"><mk-acct :user="p.user"/></span>
<router-link class="time" :to="p | notePage">
<mk-time :time="p.createdAt"/>
</router-link>
diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
index d04abfc5a7..302c5e803f 100644
--- a/src/client/app/desktop/views/components/note-preview.vue
+++ b/src/client/app/desktop/views/components/note-preview.vue
@@ -4,7 +4,7 @@
<div class="main">
<header>
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
- <span class="username">@{{ note.user | acct }}</span>
+ <span class="username"><mk-acct :user="note.user"/></span>
<router-link class="time" :to="note | notePage">
<mk-time :time="note.createdAt"/>
</router-link>
@@ -59,17 +59,20 @@ root(isDark)
> .name
margin 0 .5em 0 0
padding 0
+ overflow hidden
color isDark ? #fff : #607073
font-size 1em
font-weight bold
text-decoration none
- white-space normal
+ text-overflow ellipsis
&:hover
text-decoration underline
> .username
margin 0 .5em 0 0
+ overflow hidden
+ text-overflow ellipsis
color isDark ? #606984 : #d1d8da
> .time
diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue
index 575d605203..503982b1a8 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -4,7 +4,7 @@
<div class="main">
<header>
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
- <span class="username">@{{ note.user | acct }}</span>
+ <span class="username"><mk-acct :user="note.user"/></span>
<div class="info">
<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
<router-link class="created-at" :to="note | notePage">
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 057c3c0956..8660a5f899 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -17,7 +17,7 @@
<header>
<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
- <span class="username">@{{ p.user | acct }}</span>
+ <span class="username"><mk-acct :user="p.user"/></span>
<div class="info">
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
@@ -44,7 +44,7 @@
<div class="text">
<span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
<a class="reply" v-if="p.reply">%fa:reply%</a>
- <mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
+ <mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="os.i" :class="$style.text"/>
<a class="rp" v-if="p.renote">RP:</a>
</div>
<div class="media" v-if="p.media.length > 0">
@@ -94,6 +94,7 @@
<script lang="ts">
import Vue from 'vue';
import dateStringify from '../../../common/scripts/date-stringify';
+import canHideText from '../../../common/scripts/can-hide-text';
import parse from '../../../../../text/parse';
import MkPostFormWindow from './post-form-window.vue';
@@ -130,16 +131,17 @@ export default Vue.extend({
},
computed: {
-
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
this.note.mediaIds.length == 0 &&
this.note.poll == null);
},
+
p(): any {
return this.isRenote ? this.note.renote : this.note;
},
+
reactionsCount(): number {
return this.p.reactionCounts
? Object.keys(this.p.reactionCounts)
@@ -147,9 +149,11 @@ export default Vue.extend({
.reduce((a, b) => a + b)
: 0;
},
+
title(): string {
return dateStringify(this.p.createdAt);
},
+
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
@@ -205,6 +209,8 @@ export default Vue.extend({
},
methods: {
+ canHideText,
+
capture(withHandler = false) {
if ((this as any).os.isSignedIn) {
this.connection.send({
@@ -214,6 +220,7 @@ export default Vue.extend({
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
}
},
+
decapture(withHandler = false) {
if ((this as any).os.isSignedIn) {
this.connection.send({
@@ -223,9 +230,11 @@ export default Vue.extend({
if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
}
},
+
onStreamConnected() {
this.capture();
},
+
onStreamNoteUpdated(data) {
const note = data.note;
if (note.id == this.note.id) {
@@ -234,28 +243,33 @@ export default Vue.extend({
this.note.renote = note;
}
},
+
reply() {
(this as any).os.new(MkPostFormWindow, {
reply: this.p
});
},
+
renote() {
(this as any).os.new(MkRenoteFormWindow, {
note: this.p
});
},
+
react() {
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.p
});
},
+
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
note: this.p
});
},
+
onKeydown(e) {
let shouldBeCancel = true;
@@ -336,6 +350,7 @@ root(isDark)
align-items center
padding 16px 32px
line-height 28px
+ white-space pre
color #9dbb00
background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
diff --git a/src/client/app/desktop/views/components/settings.api.vue b/src/client/app/desktop/views/components/settings.api.vue
index 377f2e689b..b22ee6cdab 100644
--- a/src/client/app/desktop/views/components/settings.api.vue
+++ b/src/client/app/desktop/views/components/settings.api.vue
@@ -1,6 +1,6 @@
<template>
<div class="root api">
- <p>Token: <code>{{ os.i.token }}</code></p>
+ <p>%i18n:@token% <code>{{ os.i.token }}</code></p>
<p>%i18n:@intro%</p>
<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:@caution%</p></div>
<p>%i18n:@regeneration-of-token%</p>
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 9439ded2fc..4e5e281fd0 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -80,10 +80,11 @@
<el-option label="自動" value=""/>
</el-option-group>
<el-option-group label="言語を指定">
- <el-option label="ja-JP" value="ja"/>
- <el-option label="en-US" value="en"/>
+ <el-option label="ja" value="ja"/>
+ <el-option label="en" value="en"/>
<el-option label="fr" value="fr"/>
<el-option label="pl" value="pl"/>
+ <el-option label="de" value="de"/>
</el-option-group>
</el-select>
<div class="none ui info">
@@ -100,7 +101,7 @@
</section>
<section class="notification" v-show="page == 'notification'">
- <h1>通知</h1>
+ <h1>%i18n:@notification%</h1>
<mk-switch v-model="os.i.settings.autoWatch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
<span>リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。</span>
</mk-switch>
@@ -117,7 +118,7 @@
</section>
<section class="apps" v-show="page == 'apps'">
- <h1>アプリケーション</h1>
+ <h1>%i18n:@apps%</h1>
<x-apps/>
</section>
@@ -137,7 +138,7 @@
</section>
<section class="signin" v-show="page == 'security'">
- <h1>サインイン履歴</h1>
+ <h1>%i18n:@signin%</h1>
<x-signins/>
</section>
diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue
index d082610132..585c0a864f 100644
--- a/src/client/app/desktop/views/components/user-lists-window.vue
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -3,7 +3,7 @@
<span slot="header">%fa:list% リスト</span>
<div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="_darkmode_">
- <button class="ui" @click="add">リストを作成</button>
+ <button class="ui" @click="add">%i18n:@create-list%</button>
<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a>
</div>
</mk-window>
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index cc5e021390..f40e60dff9 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -10,13 +10,13 @@
<div class="description">{{ u.description }}</div>
<div class="status">
<div>
- <p>投稿</p><a>{{ u.notesCount }}</a>
+ <p>%i18n:@notes%</p><a>{{ u.notesCount }}</a>
</div>
<div>
- <p>フォロー</p><a>{{ u.followingCount }}</a>
+ <p>%i18n:@following%</p><a>{{ u.followingCount }}</a>
</div>
<div>
- <p>フォロワー</p><a>{{ u.followersCount }}</a>
+ <p>%i18n:@followers%</p><a>{{ u.followersCount }}</a>
</div>
</div>
<mk-follow-button v-if="os.isSignedIn && user.id != os.i.id" :user="u"/>
diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue
index 13d0d07bbc..1ed5c33b13 100644
--- a/src/client/app/desktop/views/components/users-list.vue
+++ b/src/client/app/desktop/views/components/users-list.vue
@@ -2,8 +2,8 @@
<div class="mk-users-list">
<nav>
<div>
- <span :data-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span>
- <span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span>
+ <span :data-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span>
+ <span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@iknow%<span>{{ youKnowCount }}</span></span>
</div>
</nav>
<div class="users" v-if="!fetching && users.length != 0">
@@ -12,13 +12,13 @@
</div>
</div>
<button class="more" v-if="!fetching && next != null" @click="more" :disabled="moreFetching">
- <span v-if="!moreFetching">もっと</span>
- <span v-if="moreFetching">読み込み中<mk-ellipsis/></span>
+ <span v-if="!moreFetching">%i18n:@load-more%</span>
+ <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
</button>
<p class="no" v-if="!fetching && users.length == 0">
<slot></slot>
</p>
- <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
+ <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@fetching%<mk-ellipsis/></p>
</div>
</template>
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index 60dc15b15d..edb248dac7 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -29,7 +29,7 @@ export default Vue.extend({
style(): any {
if (this.user.bannerUrl == null) return {};
return {
- backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
+ backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
backgroundImage: `url(${ this.user.bannerUrl })`
};
}
diff --git a/src/client/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts
index 72919c6505..0634c52642 100644
--- a/src/client/app/mobile/api/post.ts
+++ b/src/client/app/mobile/api/post.ts
@@ -1,43 +1,24 @@
import PostForm from '../views/components/post-form.vue';
-//import RenoteForm from '../views/components/renote-form.vue';
-import getNoteSummary from '../../../../renderers/get-note-summary';
export default (os) => (opts) => {
const o = opts || {};
- if (o.renote) {
- /*const vm = new RenoteForm({
- propsData: {
- renote: o.renote
- }
- }).$mount();
- vm.$once('cancel', recover);
- vm.$once('note', recover);
- document.body.appendChild(vm.$el);*/
+ const app = document.getElementById('app');
+ app.style.display = 'none';
- const text = window.prompt(`「${getNoteSummary(o.renote)}」をRenote`);
- if (text == null) return;
- os.api('notes/create', {
- renoteId: o.renote.id,
- text: text == '' ? undefined : text
- });
- } else {
- const app = document.getElementById('app');
- app.style.display = 'none';
+ function recover() {
+ app.style.display = 'block';
+ }
- function recover() {
- app.style.display = 'block';
+ const vm = new PostForm({
+ parent: os.app,
+ propsData: {
+ reply: o.reply,
+ renote: o.renote
}
-
- const vm = new PostForm({
- parent: os.app,
- propsData: {
- reply: o.reply
- }
- }).$mount();
- vm.$once('cancel', recover);
- vm.$once('note', recover);
- document.body.appendChild(vm.$el);
- (vm as any).focus();
- }
+ }).$mount();
+ vm.$once('cancel', recover);
+ vm.$once('note', recover);
+ document.body.appendChild(vm.$el);
+ (vm as any).focus();
};
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index 2e9805e0d0..1405139be6 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -23,6 +23,7 @@ import MkUser from './views/pages/user.vue';
import MkSelectDrive from './views/pages/selectdrive.vue';
import MkDrive from './views/pages/drive.vue';
import MkNotifications from './views/pages/notifications.vue';
+import MkWidgets from './views/pages/widgets.vue';
import MkMessaging from './views/pages/messaging.vue';
import MkMessagingRoom from './views/pages/messaging-room.vue';
import MkNote from './views/pages/note.vue';
@@ -56,6 +57,7 @@ init((launch) => {
{ path: '/i/settings', component: MkSettings },
{ path: '/i/settings/profile', component: MkProfileSetting },
{ path: '/i/notifications', name: 'notifications', component: MkNotifications },
+ { path: '/i/widgets', name: 'widgets', component: MkWidgets },
{ path: '/i/messaging', name: 'messaging', component: MkMessaging },
{ path: '/i/messaging/:user', component: MkMessagingRoom },
{ path: '/i/drive', name: 'drive', component: MkDrive },
diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue
index 764822e98c..ddf17d2723 100644
--- a/src/client/app/mobile/views/components/drive.file-detail.vue
+++ b/src/client/app/mobile/views/components/drive.file-detail.vue
@@ -86,7 +86,7 @@ export default Vue.extend({
return this.file.type.split('/')[0];
},
style(): any {
- return this.file.properties.avgColor ? {
+ return this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? {
'background-color': `rgb(${ this.file.properties.avgColor.join(',') })`
} : {};
}
diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue
index 7d1957042b..94c8ae3535 100644
--- a/src/client/app/mobile/views/components/drive.file.vue
+++ b/src/client/app/mobile/views/components/drive.file.vue
@@ -42,7 +42,7 @@ export default Vue.extend({
},
thumbnail(): any {
return {
- 'background-color': this.file.properties.avgColor ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
+ 'background-color': this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
'background-image': `url(${this.file.url}?thumbnail&size=128)`
};
}
diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue
index 92d1cdc6f5..9e0f8e5f7e 100644
--- a/src/client/app/mobile/views/components/media-image.vue
+++ b/src/client/app/mobile/views/components/media-image.vue
@@ -18,7 +18,7 @@ export default Vue.extend({
computed: {
style(): any {
return {
- 'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
+ 'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
};
}
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
index ec11f23315..b3ab088ffe 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -69,8 +69,9 @@ root(isDark)
text-decoration underline
> .username
- text-align left
margin 0 .5em 0 0
+ overflow hidden
+ text-overflow ellipsis
color isDark ? #606984 : #d1d8da
> .time
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index d66f5a1016..77a766f327 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -41,7 +41,7 @@
<div class="text">
<span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
<a class="reply" v-if="p.reply">%fa:reply%</a>
- <mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
+ <mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="os.i" :class="$style.text"/>
<a class="rp" v-if="p.renote != null">RP:</a>
</div>
<div class="media" v-if="p.media.length > 0">
@@ -85,6 +85,7 @@
<script lang="ts">
import Vue from 'vue';
import parse from '../../../../../text/parse';
+import canHideText from '../../../common/scripts/can-hide-text';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
@@ -112,9 +113,11 @@ export default Vue.extend({
this.note.mediaIds.length == 0 &&
this.note.poll == null);
},
+
p(): any {
return this.isRenote ? this.note.renote : this.note;
},
+
reactionsCount(): number {
return this.p.reactionCounts
? Object.keys(this.p.reactionCounts)
@@ -122,6 +125,7 @@ export default Vue.extend({
.reduce((a, b) => a + b)
: 0;
},
+
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
@@ -177,6 +181,8 @@ export default Vue.extend({
},
methods: {
+ canHideText,
+
capture(withHandler = false) {
if ((this as any).os.isSignedIn) {
this.connection.send({
@@ -186,6 +192,7 @@ export default Vue.extend({
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
}
},
+
decapture(withHandler = false) {
if ((this as any).os.isSignedIn) {
this.connection.send({
@@ -195,9 +202,11 @@ export default Vue.extend({
if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
}
},
+
onStreamConnected() {
this.capture();
},
+
onStreamNoteUpdated(data) {
const note = data.note;
if (note.id == this.note.id) {
@@ -206,16 +215,19 @@ export default Vue.extend({
this.note.renote = note;
}
},
+
reply() {
(this as any).apis.post({
reply: this.p
});
},
+
renote() {
(this as any).apis.post({
renote: this.p
});
},
+
react() {
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
@@ -223,6 +235,7 @@ export default Vue.extend({
compact: true
});
},
+
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
@@ -255,6 +268,7 @@ root(isDark)
align-items center
padding 8px 16px
line-height 28px
+ white-space pre
color #9dbb00
background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
index 53e232e521..e77698dea9 100644
--- a/src/client/app/mobile/views/components/notes.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -1,7 +1,5 @@
<template>
<div class="mk-notes">
- <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
-
<slot name="head"></slot>
<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
@@ -71,6 +69,16 @@ export default Vue.extend({
}
},
+ watch: {
+ queue(x) {
+ if (x.length > 0) {
+ this.$store.commit('indicate', true);
+ } else {
+ this.$store.commit('indicate', false);
+ }
+ }
+ },
+
mounted() {
document.addEventListener('visibilitychange', this.onVisibilitychange, false);
window.addEventListener('scroll', this.onScroll);
@@ -238,13 +246,6 @@ root(isDark)
[data-fa]
margin-right 8px
- > .newer-indicator
- position -webkit-sticky
- position sticky
- z-index 100
- height 3px
- background $theme-color
-
> .init
padding 64px 0
text-align center
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index 6d80b3046b..0bb498e5d7 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -5,17 +5,22 @@
<div>
<span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span>
<span class="geo" v-if="geo">%fa:map-marker-alt%</span>
- <button class="submit" :disabled="posting" @click="post">{{ reply ? '返信' : '%i18n:!@submit%' }}</button>
+ <button class="submit" :disabled="posting" @click="post">
+ <template v-if="reply">%i18n:@reply%</template>
+ <template v-else-if="renote">%i18n:@renote%</template>
+ <template v-else>%i18n:@submit%</template>
+ </button>
</div>
</header>
<div class="form">
<mk-note-preview v-if="reply" :note="reply"/>
+ <mk-note-preview v-if="renote" :note="renote"/>
<div v-if="visibility == 'specified'" class="visibleUsers">
<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
<a @click="addVisibleUser">+ユーザーを追加</a>
</div>
<input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)">
- <textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:!@reply-placeholder%' : '%i18n:!@note-placeholder%'"></textarea>
+ <textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:!@reply-placeholder%' : renote ? '%i18n:!@renote-placeholder%' : '%i18n:!@note-placeholder%'"></textarea>
<div class="attaches" v-show="files.length != 0">
<x-draggable class="files" :list="files" :options="{ animation: 150 }">
<div class="file" v-for="file in files" :key="file.id">
@@ -51,7 +56,7 @@ export default Vue.extend({
MkVisibilityChooser
},
- props: ['reply'],
+ props: ['reply', 'renote'],
data() {
return {
@@ -177,6 +182,7 @@ export default Vue.extend({
text: this.text == '' ? undefined : this.text,
mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined,
+ renoteId: this.renote ? this.renote.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
cw: this.useCw ? this.cw || '' : undefined,
geo: this.geo ? {
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index 509463333d..a49462b159 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -13,6 +13,7 @@
<slot name="func"></slot>
</div>
</div>
+ <div class="indicator" v-show="$store.state.indicate"></div>
</div>
</template>
@@ -156,6 +157,10 @@ root(isDark)
&, *
user-select none
+ > .indicator
+ height 3px
+ background $theme-color
+
> .main
color rgba(#fff, 0.9)
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index 5c65d52237..ec42dbc99d 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -21,6 +21,7 @@
<li><router-link to="/othello" :data-active="$route.name == 'othello'">%fa:gamepad%ゲーム<template v-if="hasGameInvitations">%fa:circle%</template>%fa:angle-right%</router-link></li>
</ul>
<ul>
+ <li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'">%fa:quidditch%%i18n:@widgets%%fa:angle-right%</router-link></li>
<li><router-link to="/i/drive" :data-active="$route.name == 'drive'">%fa:cloud%%i18n:@drive%%fa:angle-right%</router-link></li>
</ul>
<ul>
diff --git a/src/client/app/mobile/views/components/widget-container.vue b/src/client/app/mobile/views/components/widget-container.vue
index 1bdc875763..a713a10621 100644
--- a/src/client/app/mobile/views/components/widget-container.vue
+++ b/src/client/app/mobile/views/components/widget-container.vue
@@ -25,27 +25,27 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-widget-container
- background #eee
+root(isDark)
+ background isDark ? #21242f : #eee
border-radius 8px
- box-shadow 0 0 0 1px rgba(#000, 0.2)
+ box-shadow 0 4px 16px rgba(#000, 0.1)
overflow hidden
- &.hideHeader
- background #fff
-
&.naked
background transparent !important
box-shadow none !important
+ &.hideHeader
+ background isDark ? #21242f : #fff
+
> header
> .title
margin 0
padding 8px 10px
font-size 15px
font-weight normal
- color #465258
- background #fff
+ color isDark ? #b8c5cc : #465258
+ background isDark ? #282c37 : #fff
border-radius 8px 8px 0 0
> [data-fa]
@@ -65,4 +65,10 @@ export default Vue.extend({
font-size 15px
color #465258
+.mk-widget-container[data-darkmode]
+ root(true)
+
+.mk-widget-container:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 27482dc215..f43454f9db 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -84,7 +84,7 @@ export default Vue.extend({
style(): any {
if (this.user.bannerUrl == null) return {};
return {
- backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
+ backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
backgroundImage: `url(${ this.user.bannerUrl })`
};
}
diff --git a/src/client/app/mobile/views/pages/dashboard.vue b/src/client/app/mobile/views/pages/widgets.vue
index a5ca6cb4a2..509ce16eef 100644
--- a/src/client/app/mobile/views/pages/dashboard.vue
+++ b/src/client/app/mobile/views/pages/widgets.vue
@@ -40,7 +40,7 @@
</x-draggable>
</template>
<template v-else>
- <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/>
+ <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true"/>
</template>
</main>
</mk-ui>
@@ -55,17 +55,24 @@ export default Vue.extend({
components: {
XDraggable
},
+
data() {
return {
showNav: false,
- widgets: [],
customizing: false,
widgetAdderSelected: null
};
},
+
+ computed: {
+ widgets(): any[] {
+ return this.$store.state.settings.data.mobileHome;
+ }
+ },
+
created() {
- if ((this as any).clientSettings.mobileHome == null) {
- Vue.set((this as any).clientSettings, 'mobileHome', [{
+ if (this.widgets.length == 0) {
+ this.widgets = [{
name: 'calendar',
id: 'a', data: {}
}, {
@@ -86,18 +93,9 @@ export default Vue.extend({
}, {
name: 'version',
id: 'g', data: {}
- }]);
- this.widgets = (this as any).clientSettings.mobileHome;
+ }];
this.saveHome();
- } else {
- this.widgets = (this as any).clientSettings.mobileHome;
}
-
- this.$watch('clientSettings', i => {
- this.widgets = (this as any).clientSettings.mobileHome;
- }, {
- deep: true
- });
},
mounted() {
@@ -105,46 +103,33 @@ export default Vue.extend({
},
methods: {
- onHomeUpdated(data) {
- if (data.home) {
- (this as any).clientSettings.mobileHome = data.home;
- this.widgets = data.home;
- } else {
- const w = (this as any).clientSettings.mobileHome.find(w => w.id == data.id);
- if (w != null) {
- w.data = data.data;
- this.$refs[w.id][0].preventSave = true;
- this.$refs[w.id][0].props = w.data;
- this.widgets = (this as any).clientSettings.mobileHome;
- }
- }
- },
hint() {
alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。');
},
+
widgetFunc(id) {
const w = this.$refs[id][0];
if (w.func) w.func();
},
+
onWidgetSort() {
this.saveHome();
},
+
addWidget() {
- const widget = {
+ this.$store.dispatch('settings/addMobileHomeWidget', {
name: this.widgetAdderSelected,
id: uuid(),
data: {}
- };
-
- this.widgets.unshift(widget);
- this.saveHome();
+ });
},
+
removeWidget(widget) {
- this.widgets = this.widgets.filter(w => w.id != widget.id);
- this.saveHome();
+ this.$store.dispatch('settings/removeMobileHomeWidget', widget);
},
+
saveHome() {
- (this as any).clientSettings.mobileHome = this.widgets;
+ this.$store.commit('settings/setMobileHome', this.widgets);
(this as any).api('i/update_mobile_home', {
home: this.widgets
});
@@ -156,17 +141,25 @@ export default Vue.extend({
<style lang="stylus" scoped>
main
margin 0 auto
+ padding 8px
max-width 500px
+ width 100%
@media (min-width 500px)
- padding 8px
+ padding 16px 8px
+
+ @media (min-width 600px)
+ padding 32px 8px
> header
padding 8px
background #fff
.widget
- margin 8px
+ margin-bottom 8px
+
+ @media (min-width 600px)
+ margin-bottom 16px
.customize-container
margin 8px
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 0bdfdef6a0..1f1189054d 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -3,6 +3,7 @@ import MiOS from './mios';
const defaultSettings = {
home: [],
+ mobileHome: [],
fetchOnScroll: true,
showMaps: true,
showPostFormOnTopOfTl: false,
@@ -23,10 +24,15 @@ export default (os: MiOS) => new Vuex.Store({
}],
state: {
+ indicate: false,
uiHeaderHeight: 0
},
mutations: {
+ indicate(state, x) {
+ state.indicate = x;
+ },
+
setUiHeaderHeight(state, height) {
state.uiHeaderHeight = height;
}
@@ -58,6 +64,25 @@ export default (os: MiOS) => new Vuex.Store({
addHomeWidget(state, widget) {
state.data.home.unshift(widget);
+ },
+
+ setMobileHome(state, data) {
+ state.data.mobileHome = data;
+ },
+
+ setMobileHomeWidget(state, x) {
+ const w = state.data.mobileHome.find(w => w.id == x.id);
+ if (w) {
+ w.data = x.data;
+ }
+ },
+
+ addMobileHomeWidget(state, widget) {
+ state.data.mobileHome.unshift(widget);
+ },
+
+ removeMobileHomeWidget(state, widget) {
+ state.data.mobileHome = state.data.mobileHome.filter(w => w.id != widget.id);
}
},
@@ -85,6 +110,22 @@ export default (os: MiOS) => new Vuex.Store({
os.api('i/update_home', {
home: ctx.state.data.home
});
+ },
+
+ addMobileHomeWidget(ctx, widget) {
+ ctx.commit('addMobileHomeWidget', widget);
+
+ os.api('i/update_mobile_home', {
+ home: ctx.state.data.mobileHome
+ });
+ },
+
+ removeMobileHomeWidget(ctx, widget) {
+ ctx.commit('removeMobileHomeWidget', widget);
+
+ os.api('i/update_mobile_home', {
+ home: ctx.state.data.mobileHome.filter(w => w.id != widget.id)
+ });
}
}
}
diff --git a/src/client/docs/api/gulpfile.ts b/src/client/docs/api/gulpfile.ts
index 31027c0be3..9980ede231 100644
--- a/src/client/docs/api/gulpfile.ts
+++ b/src/client/docs/api/gulpfile.ts
@@ -127,7 +127,7 @@ gulp.task('doc:api:endpoints', async () => {
return;
}
const i18n = new I18nReplacer(lang);
- html = html.replace(i18n.pattern, i18n.replacement.bind(null, null));
+ html = html.replace(i18n.pattern, i18n.replacement);
html = fa(html);
const htmlPath = `./built/client/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
mkdirp(path.dirname(htmlPath), (mkdirErr) => {
@@ -171,7 +171,7 @@ gulp.task('doc:api:entities', async () => {
return;
}
const i18n = new I18nReplacer(lang);
- html = html.replace(i18n.pattern, i18n.replacement.bind(null, null));
+ html = html.replace(i18n.pattern, i18n.replacement);
html = fa(html);
const htmlPath = `./built/client/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
mkdirp(path.dirname(htmlPath), (mkdirErr) => {
diff --git a/src/client/docs/gulpfile.ts b/src/client/docs/gulpfile.ts
index 5e81d6d3b5..56bf6188c8 100644
--- a/src/client/docs/gulpfile.ts
+++ b/src/client/docs/gulpfile.ts
@@ -53,7 +53,7 @@ gulp.task('doc:docs', async () => {
return;
}
const i18n = new I18nReplacer(lang);
- html = html.replace(i18n.pattern, i18n.replacement.bind(null, null));
+ html = html.replace(i18n.pattern, i18n.replacement);
html = fa(html);
const htmlPath = `./built/client/docs/${lang}/${name}.html`;
mkdirp(path.dirname(htmlPath), (mkdirErr) => {
diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
index f8cad36f9a..8a18567dc6 100644
--- a/src/models/drive-file.ts
+++ b/src/models/drive-file.ts
@@ -154,6 +154,7 @@ export const pack = (
_target = Object.assign(_target, _file.metadata);
+ _target.src = _file.metadata.url;
_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
if (_target.properties == null) _target.properties = {};
diff --git a/src/server/web/url-preview.ts b/src/server/web/url-preview.ts
index cd53837a25..99ee2eaebd 100644
--- a/src/server/web/url-preview.ts
+++ b/src/server/web/url-preview.ts
@@ -2,14 +2,20 @@ import * as Koa from 'koa';
import summaly from 'summaly';
module.exports = async (ctx: Koa.Context) => {
- const summary = await summaly(ctx.query.url);
- summary.icon = wrap(summary.icon);
- summary.thumbnail = wrap(summary.thumbnail);
+ try {
+ const summary = await summaly(ctx.query.url, {
+ followRedirects: false
+ });
+ summary.icon = wrap(summary.icon);
+ summary.thumbnail = wrap(summary.thumbnail);
- // Cache 7days
- ctx.set('Cache-Control', 'max-age=604800, immutable');
+ // Cache 7days
+ ctx.set('Cache-Control', 'max-age=604800, immutable');
- ctx.body = summary;
+ ctx.body = summary;
+ } catch (e) {
+ ctx.status = 500;
+ }
};
function wrap(url: string): string {
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index efabe345d1..bcd5bee512 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -171,6 +171,9 @@ const addFile = async (
log('calculate average color...');
+ const info = await prominence(gm(fs.createReadStream(path), name)).identify();
+ const isTransparent = info ? info['Channel depth'].Alpha != null : false;
+
const buffer = await prominence(gm(fs.createReadStream(path), name)
.setFormat('ppm')
.resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック
@@ -182,7 +185,7 @@ const addFile = async (
log(`average color is calculated: ${r}, ${g}, ${b}`);
- return [r, g, b];
+ return isTransparent ? [r, g, b, 255] : [r, g, b];
})(),
// folder
(async () => {
diff --git a/test/api.ts b/test/api.ts
deleted file mode 100644
index d8c163e920..0000000000
--- a/test/api.ts
+++ /dev/null
@@ -1,1152 +0,0 @@
-/**
- * API TESTS
- */
-
-import * as merge from 'object-assign-deep';
-
-Error.stackTraceLimit = Infinity;
-
-// During the test the env variable is set to test
-process.env.NODE_ENV = 'test';
-
-// Display detail of unhandled promise rejection
-process.on('unhandledRejection', console.dir);
-
-const fs = require('fs');
-const _chai = require('chai');
-const chaiHttp = require('chai-http');
-
-_chai.use(chaiHttp);
-
-const server = require('../built/server/api').callback();
-const db = require('../built/db/mongodb').default;
-
-const async = fn => (done) => {
- fn().then(() => {
- done();
- }, err => {
- done(err);
- });
-};
-
-const request = (endpoint, params, me?) => new Promise<any>((ok, ng) => {
- const auth = me ? {
- i: me.token
- } : {};
-
- _chai.request(server)
- .post(endpoint)
- .send(Object.assign(auth, params))
- .end((err, res) => {
- ok(res);
- });
-});
-
-describe('API', () => {
- // Reset database each test
- beforeEach(() => Promise.all([
- db.get('users').drop(),
- db.get('posts').drop(),
- db.get('driveFiles.files').drop(),
- db.get('driveFiles.chunks').drop(),
- db.get('driveFolders').drop(),
- db.get('apps').drop(),
- db.get('accessTokens').drop(),
- db.get('authSessions').drop()
- ]));
-
- describe('signup', () => {
- it('不正なユーザー名でアカウントが作成できない', async(async () => {
- const res = await request('/signup', {
- username: 'sakurako.',
- password: 'HimawariDaisuki06160907'
- });
- res.should.have.status(400);
- }));
-
- it('空のパスワードでアカウントが作成できない', async(async () => {
- const res = await request('/signup', {
- username: 'sakurako',
- password: ''
- });
- res.should.have.status(400);
- }));
-
- it('正しくアカウントが作成できる', async(async () => {
- const me = {
- username: 'sakurako',
- password: 'HimawariDaisuki06160907'
- };
- const res = await request('/signup', me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('username').eql(me.username);
- }));
-
- it('同じユーザー名のアカウントは作成できない', async(async () => {
- const user = await insertSakurako();
- const res = await request('/signup', {
- username: user.username,
- password: 'HimawariDaisuki06160907'
- });
- res.should.have.status(400);
- }));
- });
-
- describe('signin', () => {
- it('間違ったパスワードでサインインできない', async(async () => {
- const me = await insertSakurako();
- const res = await request('/signin', {
- username: me.username,
- password: 'kyoppie'
- });
- res.should.have.status(400);
- }));
-
- it('クエリをインジェクションできない', async(async () => {
- const me = await insertSakurako();
- const res = await request('/signin', {
- username: me.username,
- password: {
- $gt: ''
- }
- });
- res.should.have.status(400);
- }));
-
- it('正しい情報でサインインできる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/signin', {
- username: me.username,
- password: 'HimawariDaisuki06160907'
- });
- res.should.have.status(204);
- }));
- });
-
- describe('i/update', () => {
- it('アカウント設定を更新できる', async(async () => {
- const me = await insertSakurako({
- account: {
- profile: {
- gender: 'female'
- }
- }
- });
-
- const myName = '大室櫻子';
- const myLocation = '七森中';
- const myBirthday = '2000-09-07';
-
- const res = await request('/i/update', {
- name: myName,
- location: myLocation,
- birthday: myBirthday
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('name').eql(myName);
- res.body.should.have.nested.property('profile').a('object');
- res.body.should.have.nested.property('profile.location').eql(myLocation);
- res.body.should.have.nested.property('profile.birthday').eql(myBirthday);
- res.body.should.have.nested.property('profile.gender').eql('female');
- }));
-
- it('名前を空白にできない', async(async () => {
- const me = await insertSakurako();
- const res = await request('/i/update', {
- name: ' '
- }, me);
- res.should.have.status(400);
- }));
-
- it('誕生日の設定を削除できる', async(async () => {
- const me = await insertSakurako({
- birthday: '2000-09-07'
- });
- const res = await request('/i/update', {
- birthday: null
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.nested.property('profile').a('object');
- res.body.should.have.nested.property('profile.birthday').eql(null);
- }));
-
- it('不正な誕生日の形式で怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/i/update', {
- birthday: '2000/09/07'
- }, me);
- res.should.have.status(400);
- }));
- });
-
- describe('users/show', () => {
- it('ユーザーが取得できる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/users/show', {
- userId: me._id.toString()
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('id').eql(me._id.toString());
- }));
-
- it('ユーザーが存在しなかったら怒る', async(async () => {
- const res = await request('/users/show', {
- userId: '000000000000000000000000'
- });
- res.should.have.status(400);
- }));
-
- it('間違ったIDで怒られる', async(async () => {
- const res = await request('/users/show', {
- userId: 'kyoppie'
- });
- res.should.have.status(400);
- }));
- });
-
- describe('posts/create', () => {
- it('投稿できる', async(async () => {
- const me = await insertSakurako();
- const post = {
- text: 'ひまわりー'
- };
- const res = await request('/posts/create', post, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('createdPost');
- res.body.createdPost.should.have.property('text').eql(post.text);
- }));
-
- it('ファイルを添付できる', async(async () => {
- const me = await insertSakurako();
- const file = await insertDriveFile({
- userId: me._id
- });
- const res = await request('/posts/create', {
- mediaIds: [file._id.toString()]
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('createdPost');
- res.body.createdPost.should.have.property('mediaIds').eql([file._id.toString()]);
- }));
-
- it('他人のファイルは添付できない', async(async () => {
- const me = await insertSakurako();
- const hima = await insertHimawari();
- const file = await insertDriveFile({
- userId: hima._id
- });
- const res = await request('/posts/create', {
- mediaIds: [file._id.toString()]
- }, me);
- res.should.have.status(400);
- }));
-
- it('存在しないファイルは添付できない', async(async () => {
- const me = await insertSakurako();
- const res = await request('/posts/create', {
- mediaIds: ['000000000000000000000000']
- }, me);
- res.should.have.status(400);
- }));
-
- it('不正なファイルIDで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/posts/create', {
- mediaIds: ['kyoppie']
- }, me);
- res.should.have.status(400);
- }));
-
- it('返信できる', async(async () => {
- const hima = await insertHimawari();
- const himaPost = await db.get('posts').insert({
- userId: hima._id,
- text: 'ひま'
- });
-
- const me = await insertSakurako();
- const post = {
- text: 'さく',
- replyId: himaPost._id.toString()
- };
- const res = await request('/posts/create', post, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('createdPost');
- res.body.createdPost.should.have.property('text').eql(post.text);
- res.body.createdPost.should.have.property('replyId').eql(post.replyId);
- res.body.createdPost.should.have.property('reply');
- res.body.createdPost.reply.should.have.property('text').eql(himaPost.text);
- }));
-
- it('repostできる', async(async () => {
- const hima = await insertHimawari();
- const himaPost = await db.get('posts').insert({
- userId: hima._id,
- text: 'こらっさくらこ!'
- });
-
- const me = await insertSakurako();
- const post = {
- repostId: himaPost._id.toString()
- };
- const res = await request('/posts/create', post, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('createdPost');
- res.body.createdPost.should.have.property('repostId').eql(post.repostId);
- res.body.createdPost.should.have.property('repost');
- res.body.createdPost.repost.should.have.property('text').eql(himaPost.text);
- }));
-
- it('引用repostできる', async(async () => {
- const hima = await insertHimawari();
- const himaPost = await db.get('posts').insert({
- userId: hima._id,
- text: 'こらっさくらこ!'
- });
-
- const me = await insertSakurako();
- const post = {
- text: 'さく',
- repostId: himaPost._id.toString()
- };
- const res = await request('/posts/create', post, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('createdPost');
- res.body.createdPost.should.have.property('text').eql(post.text);
- res.body.createdPost.should.have.property('repostId').eql(post.repostId);
- res.body.createdPost.should.have.property('repost');
- res.body.createdPost.repost.should.have.property('text').eql(himaPost.text);
- }));
-
- it('文字数ぎりぎりで怒られない', async(async () => {
- const me = await insertSakurako();
- const post = {
- text: '!'.repeat(1000)
- };
- const res = await request('/posts/create', post, me);
- res.should.have.status(200);
- }));
-
- it('文字数オーバーで怒られる', async(async () => {
- const me = await insertSakurako();
- const post = {
- text: '!'.repeat(1001)
- };
- const res = await request('/posts/create', post, me);
- res.should.have.status(400);
- }));
-
- it('存在しないリプライ先で怒られる', async(async () => {
- const me = await insertSakurako();
- const post = {
- text: 'さく',
- replyId: '000000000000000000000000'
- };
- const res = await request('/posts/create', post, me);
- res.should.have.status(400);
- }));
-
- it('存在しないrepost対象で怒られる', async(async () => {
- const me = await insertSakurako();
- const post = {
- repostId: '000000000000000000000000'
- };
- const res = await request('/posts/create', post, me);
- res.should.have.status(400);
- }));
-
- it('不正なリプライ先IDで怒られる', async(async () => {
- const me = await insertSakurako();
- const post = {
- text: 'さく',
- replyId: 'kyoppie'
- };
- const res = await request('/posts/create', post, me);
- res.should.have.status(400);
- }));
-
- it('不正なrepost対象IDで怒られる', async(async () => {
- const me = await insertSakurako();
- const post = {
- repostId: 'kyoppie'
- };
- const res = await request('/posts/create', post, me);
- res.should.have.status(400);
- }));
-
- it('投票を添付できる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/posts/create', {
- text: 'インデントするなら?',
- poll: {
- choices: ['スペース', 'タブ']
- }
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('createdPost');
- res.body.createdPost.should.have.property('poll');
- }));
-
- it('投票の選択肢が無くて怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/posts/create', {
- poll: {}
- }, me);
- res.should.have.status(400);
- }));
-
- it('投票の選択肢が無くて怒られる (空の配列)', async(async () => {
- const me = await insertSakurako();
- const res = await request('/posts/create', {
- poll: {
- choices: []
- }
- }, me);
- res.should.have.status(400);
- }));
-
- it('投票の選択肢が1つで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/posts/create', {
- poll: {
- choices: ['Strawberry Pasta']
- }
- }, me);
- res.should.have.status(400);
- }));
- });
-
- describe('posts/show', () => {
- it('投稿が取得できる', async(async () => {
- const me = await insertSakurako();
- const myPost = await db.get('posts').insert({
- userId: me._id,
- text: 'お腹ペコい'
- });
- const res = await request('/posts/show', {
- postId: myPost._id.toString()
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('id').eql(myPost._id.toString());
- }));
-
- it('投稿が存在しなかったら怒る', async(async () => {
- const res = await request('/posts/show', {
- postId: '000000000000000000000000'
- });
- res.should.have.status(400);
- }));
-
- it('間違ったIDで怒られる', async(async () => {
- const res = await request('/posts/show', {
- postId: 'kyoppie'
- });
- res.should.have.status(400);
- }));
- });
-
- describe('posts/reactions/create', () => {
- it('リアクションできる', async(async () => {
- const hima = await insertHimawari();
- const himaPost = await db.get('posts').insert({
- userId: hima._id,
- text: 'ひま'
- });
-
- const me = await insertSakurako();
- const res = await request('/posts/reactions/create', {
- postId: himaPost._id.toString(),
- reaction: 'like'
- }, me);
- res.should.have.status(204);
- }));
-
- it('自分の投稿にはリアクションできない', async(async () => {
- const me = await insertSakurako();
- const myPost = await db.get('posts').insert({
- userId: me._id,
- text: 'お腹ペコい'
- });
-
- const res = await request('/posts/reactions/create', {
- postId: myPost._id.toString(),
- reaction: 'like'
- }, me);
- res.should.have.status(400);
- }));
-
- it('二重にリアクションできない', async(async () => {
- const hima = await insertHimawari();
- const himaPost = await db.get('posts').insert({
- userId: hima._id,
- text: 'ひま'
- });
-
- const me = await insertSakurako();
- await db.get('postReactions').insert({
- userId: me._id,
- postId: himaPost._id,
- reaction: 'like'
- });
-
- const res = await request('/posts/reactions/create', {
- postId: himaPost._id.toString(),
- reaction: 'like'
- }, me);
- res.should.have.status(400);
- }));
-
- it('存在しない投稿にはリアクションできない', async(async () => {
- const me = await insertSakurako();
- const res = await request('/posts/reactions/create', {
- postId: '000000000000000000000000',
- reaction: 'like'
- }, me);
- res.should.have.status(400);
- }));
-
- it('空のパラメータで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/posts/reactions/create', {}, me);
- res.should.have.status(400);
- }));
-
- it('間違ったIDで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/posts/reactions/create', {
- postId: 'kyoppie',
- reaction: 'like'
- }, me);
- res.should.have.status(400);
- }));
- });
-
- describe('posts/reactions/delete', () => {
- it('リアクションをキャンセルできる', async(async () => {
- const hima = await insertHimawari();
- const himaPost = await db.get('posts').insert({
- userId: hima._id,
- text: 'ひま'
- });
-
- const me = await insertSakurako();
- await db.get('postReactions').insert({
- userId: me._id,
- postId: himaPost._id,
- reaction: 'like'
- });
-
- const res = await request('/posts/reactions/delete', {
- postId: himaPost._id.toString()
- }, me);
- res.should.have.status(204);
- }));
-
- it('リアクションしていない投稿はリアクションをキャンセルできない', async(async () => {
- const hima = await insertHimawari();
- const himaPost = await db.get('posts').insert({
- userId: hima._id,
- text: 'ひま'
- });
-
- const me = await insertSakurako();
- const res = await request('/posts/reactions/delete', {
- postId: himaPost._id.toString()
- }, me);
- res.should.have.status(400);
- }));
-
- it('存在しない投稿はリアクションをキャンセルできない', async(async () => {
- const me = await insertSakurako();
- const res = await request('/posts/reactions/delete', {
- postId: '000000000000000000000000'
- }, me);
- res.should.have.status(400);
- }));
-
- it('空のパラメータで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/posts/reactions/delete', {}, me);
- res.should.have.status(400);
- }));
-
- it('間違ったIDで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/posts/reactions/delete', {
- postId: 'kyoppie'
- }, me);
- res.should.have.status(400);
- }));
- });
-
- describe('following/create', () => {
- it('フォローできる', async(async () => {
- const hima = await insertHimawari();
- const me = await insertSakurako();
- const res = await request('/following/create', {
- userId: hima._id.toString()
- }, me);
- res.should.have.status(204);
- }));
-
- it('既にフォローしている場合は怒る', async(async () => {
- const hima = await insertHimawari();
- const me = await insertSakurako();
- await db.get('following').insert({
- followeeId: hima._id,
- followerId: me._id
- });
- const res = await request('/following/create', {
- userId: hima._id.toString()
- }, me);
- res.should.have.status(400);
- }));
-
- it('存在しないユーザーはフォローできない', async(async () => {
- const me = await insertSakurako();
- const res = await request('/following/create', {
- userId: '000000000000000000000000'
- }, me);
- res.should.have.status(400);
- }));
-
- it('自分自身はフォローできない', async(async () => {
- const me = await insertSakurako();
- const res = await request('/following/create', {
- userId: me._id.toString()
- }, me);
- res.should.have.status(400);
- }));
-
- it('空のパラメータで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/following/create', {}, me);
- res.should.have.status(400);
- }));
-
- it('間違ったIDで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/following/create', {
- userId: 'kyoppie'
- }, me);
- res.should.have.status(400);
- }));
- });
-
- describe('following/delete', () => {
- it('フォロー解除できる', async(async () => {
- const hima = await insertHimawari();
- const me = await insertSakurako();
- await db.get('following').insert({
- followeeId: hima._id,
- followerId: me._id
- });
- const res = await request('/following/delete', {
- userId: hima._id.toString()
- }, me);
- res.should.have.status(204);
- }));
-
- it('フォローしていない場合は怒る', async(async () => {
- const hima = await insertHimawari();
- const me = await insertSakurako();
- const res = await request('/following/delete', {
- userId: hima._id.toString()
- }, me);
- res.should.have.status(400);
- }));
-
- it('存在しないユーザーはフォロー解除できない', async(async () => {
- const me = await insertSakurako();
- const res = await request('/following/delete', {
- userId: '000000000000000000000000'
- }, me);
- res.should.have.status(400);
- }));
-
- it('自分自身はフォロー解除できない', async(async () => {
- const me = await insertSakurako();
- const res = await request('/following/delete', {
- userId: me._id.toString()
- }, me);
- res.should.have.status(400);
- }));
-
- it('空のパラメータで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/following/delete', {}, me);
- res.should.have.status(400);
- }));
-
- it('間違ったIDで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/following/delete', {
- userId: 'kyoppie'
- }, me);
- res.should.have.status(400);
- }));
- });
-
- describe('drive', () => {
- it('ドライブ情報を取得できる', async(async () => {
- const me = await insertSakurako();
- await insertDriveFile({
- userId: me._id,
- datasize: 256
- });
- await insertDriveFile({
- userId: me._id,
- datasize: 512
- });
- await insertDriveFile({
- userId: me._id,
- datasize: 1024
- });
- const res = await request('/drive', {}, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('usage').eql(1792);
- }));
- });
-
- describe('drive/files/create', () => {
- it('ファイルを作成できる', async(async () => {
- const me = await insertSakurako();
- const res = await _chai.request(server)
- .post('/drive/files/create')
- .field('i', me.token)
- .attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('name').eql('Lenna.png');
- }));
-
- it('ファイル無しで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/drive/files/create', {}, me);
- res.should.have.status(400);
- }));
- });
-
- describe('drive/files/update', () => {
- it('名前を更新できる', async(async () => {
- const me = await insertSakurako();
- const file = await insertDriveFile({
- userId: me._id
- });
- const newName = 'いちごパスタ.png';
- const res = await request('/drive/files/update', {
- fileId: file._id.toString(),
- name: newName
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('name').eql(newName);
- }));
-
- it('他人のファイルは更新できない', async(async () => {
- const me = await insertSakurako();
- const hima = await insertHimawari();
- const file = await insertDriveFile({
- userId: hima._id
- });
- const res = await request('/drive/files/update', {
- fileId: file._id.toString(),
- name: 'いちごパスタ.png'
- }, me);
- res.should.have.status(400);
- }));
-
- it('親フォルダを更新できる', async(async () => {
- const me = await insertSakurako();
- const file = await insertDriveFile({
- userId: me._id
- });
- const folder = await insertDriveFolder({
- userId: me._id
- });
- const res = await request('/drive/files/update', {
- fileId: file._id.toString(),
- folderId: folder._id.toString()
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('folderId').eql(folder._id.toString());
- }));
-
- it('親フォルダを無しにできる', async(async () => {
- const me = await insertSakurako();
- const file = await insertDriveFile({
- userId: me._id,
- folderId: '000000000000000000000000'
- });
- const res = await request('/drive/files/update', {
- fileId: file._id.toString(),
- folderId: null
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('folderId').eql(null);
- }));
-
- it('他人のフォルダには入れられない', async(async () => {
- const me = await insertSakurako();
- const hima = await insertHimawari();
- const file = await insertDriveFile({
- userId: me._id
- });
- const folder = await insertDriveFolder({
- userId: hima._id
- });
- const res = await request('/drive/files/update', {
- fileId: file._id.toString(),
- folderId: folder._id.toString()
- }, me);
- res.should.have.status(400);
- }));
-
- it('存在しないフォルダで怒られる', async(async () => {
- const me = await insertSakurako();
- const file = await insertDriveFile({
- userId: me._id
- });
- const res = await request('/drive/files/update', {
- fileId: file._id.toString(),
- folderId: '000000000000000000000000'
- }, me);
- res.should.have.status(400);
- }));
-
- it('不正なフォルダIDで怒られる', async(async () => {
- const me = await insertSakurako();
- const file = await insertDriveFile({
- userId: me._id
- });
- const res = await request('/drive/files/update', {
- fileId: file._id.toString(),
- folderId: 'kyoppie'
- }, me);
- res.should.have.status(400);
- }));
-
- it('ファイルが存在しなかったら怒る', async(async () => {
- const me = await insertSakurako();
- const res = await request('/drive/files/update', {
- fileId: '000000000000000000000000',
- name: 'いちごパスタ.png'
- }, me);
- res.should.have.status(400);
- }));
-
- it('間違ったIDで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/drive/files/update', {
- fileId: 'kyoppie',
- name: 'いちごパスタ.png'
- }, me);
- res.should.have.status(400);
- }));
- });
-
- describe('drive/folders/create', () => {
- it('フォルダを作成できる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/drive/folders/create', {
- name: 'my folder'
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('name').eql('my folder');
- }));
- });
-
- describe('drive/folders/update', () => {
- it('名前を更新できる', async(async () => {
- const me = await insertSakurako();
- const folder = await insertDriveFolder({
- userId: me._id
- });
- const res = await request('/drive/folders/update', {
- folderId: folder._id.toString(),
- name: 'new name'
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('name').eql('new name');
- }));
-
- it('他人のフォルダを更新できない', async(async () => {
- const me = await insertSakurako();
- const hima = await insertHimawari();
- const folder = await insertDriveFolder({
- userId: hima._id
- });
- const res = await request('/drive/folders/update', {
- folderId: folder._id.toString(),
- name: 'new name'
- }, me);
- res.should.have.status(400);
- }));
-
- it('親フォルダを更新できる', async(async () => {
- const me = await insertSakurako();
- const folder = await insertDriveFolder({
- userId: me._id
- });
- const parentFolder = await insertDriveFolder({
- userId: me._id
- });
- const res = await request('/drive/folders/update', {
- folderId: folder._id.toString(),
- parentId: parentFolder._id.toString()
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('parentId').eql(parentFolder._id.toString());
- }));
-
- it('親フォルダを無しに更新できる', async(async () => {
- const me = await insertSakurako();
- const folder = await insertDriveFolder({
- userId: me._id,
- parentId: '000000000000000000000000'
- });
- const res = await request('/drive/folders/update', {
- folderId: folder._id.toString(),
- parentId: null
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('parentId').eql(null);
- }));
-
- it('他人のフォルダを親フォルダに設定できない', async(async () => {
- const me = await insertSakurako();
- const hima = await insertHimawari();
- const folder = await insertDriveFolder({
- userId: me._id
- });
- const parentFolder = await insertDriveFolder({
- userId: hima._id
- });
- const res = await request('/drive/folders/update', {
- folderId: folder._id.toString(),
- parentId: parentFolder._id.toString()
- }, me);
- res.should.have.status(400);
- }));
-
- it('フォルダが循環するような構造にできない', async(async () => {
- const me = await insertSakurako();
- const folder = await insertDriveFolder();
- const parentFolder = await insertDriveFolder({
- parentId: folder._id
- });
- const res = await request('/drive/folders/update', {
- folderId: folder._id.toString(),
- parentId: parentFolder._id.toString()
- }, me);
- res.should.have.status(400);
- }));
-
- it('フォルダが循環するような構造にできない(再帰的)', async(async () => {
- const me = await insertSakurako();
- const folderA = await insertDriveFolder();
- const folderB = await insertDriveFolder({
- parentId: folderA._id
- });
- const folderC = await insertDriveFolder({
- parentId: folderB._id
- });
- const res = await request('/drive/folders/update', {
- folderId: folderA._id.toString(),
- parentId: folderC._id.toString()
- }, me);
- res.should.have.status(400);
- }));
-
- it('存在しない親フォルダを設定できない', async(async () => {
- const me = await insertSakurako();
- const folder = await insertDriveFolder();
- const res = await request('/drive/folders/update', {
- folderId: folder._id.toString(),
- parentId: '000000000000000000000000'
- }, me);
- res.should.have.status(400);
- }));
-
- it('不正な親フォルダIDで怒られる', async(async () => {
- const me = await insertSakurako();
- const folder = await insertDriveFolder();
- const res = await request('/drive/folders/update', {
- folderId: folder._id.toString(),
- parentId: 'kyoppie'
- }, me);
- res.should.have.status(400);
- }));
-
- it('存在しないフォルダを更新できない', async(async () => {
- const me = await insertSakurako();
- const res = await request('/drive/folders/update', {
- folderId: '000000000000000000000000'
- }, me);
- res.should.have.status(400);
- }));
-
- it('不正なフォルダIDで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/drive/folders/update', {
- folderId: 'kyoppie'
- }, me);
- res.should.have.status(400);
- }));
- });
-
- describe('messaging/messages/create', () => {
- it('メッセージを送信できる', async(async () => {
- const me = await insertSakurako();
- const hima = await insertHimawari();
- const res = await request('/messaging/messages/create', {
- userId: hima._id.toString(),
- text: 'Hey hey ひまわり'
- }, me);
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('text').eql('Hey hey ひまわり');
- }));
-
- it('自分自身にはメッセージを送信できない', async(async () => {
- const me = await insertSakurako();
- const res = await request('/messaging/messages/create', {
- userId: me._id.toString(),
- text: 'Yo'
- }, me);
- res.should.have.status(400);
- }));
-
- it('存在しないユーザーにはメッセージを送信できない', async(async () => {
- const me = await insertSakurako();
- const res = await request('/messaging/messages/create', {
- userId: '000000000000000000000000',
- text: 'Yo'
- }, me);
- res.should.have.status(400);
- }));
-
- it('不正なユーザーIDで怒られる', async(async () => {
- const me = await insertSakurako();
- const res = await request('/messaging/messages/create', {
- userId: 'kyoppie',
- text: 'Yo'
- }, me);
- res.should.have.status(400);
- }));
-
- it('テキストが無くて怒られる', async(async () => {
- const me = await insertSakurako();
- const hima = await insertHimawari();
- const res = await request('/messaging/messages/create', {
- userId: hima._id.toString()
- }, me);
- res.should.have.status(400);
- }));
-
- it('文字数オーバーで怒られる', async(async () => {
- const me = await insertSakurako();
- const hima = await insertHimawari();
- const res = await request('/messaging/messages/create', {
- userId: hima._id.toString(),
- text: '!'.repeat(1001)
- }, me);
- res.should.have.status(400);
- }));
- });
-
- describe('auth/session/generate', () => {
- it('認証セッションを作成できる', async(async () => {
- const app = await insertApp();
- const res = await request('/auth/session/generate', {
- appSecret: app.secret
- });
- res.should.have.status(200);
- res.body.should.be.a('object');
- res.body.should.have.property('token');
- res.body.should.have.property('url');
- }));
-
- it('appSecret 無しで怒られる', async(async () => {
- const res = await request('/auth/session/generate', {});
- res.should.have.status(400);
- }));
-
- it('誤った appSecret で怒られる', async(async () => {
- const res = await request('/auth/session/generate', {
- appSecret: 'kyoppie'
- });
- res.should.have.status(400);
- }));
- });
-});
-
-function insertSakurako(opts?) {
- return db.get('users').insert(merge({
- username: 'sakurako',
- usernameLower: 'sakurako',
- account: {
- keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n',
- token: '!00000000000000000000000000000000',
- password: '$2a$08$FnHXg3tP.M/kINWgQSXNqeoBsiVrkj.ecXX8mW9rfBzMRkibYfjYy', // HimawariDaisuki06160907
- profile: {},
- settings: {},
- clientSettings: {}
- }
- }, opts));
-}
-
-function insertHimawari(opts?) {
- return db.get('users').insert(merge({
- username: 'himawari',
- usernameLower: 'himawari',
- account: {
- keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n',
- token: '!00000000000000000000000000000001',
- password: '$2a$08$OPESxR2RE/ZijjGanNKk6ezSqGFitqsbZqTjWUZPLhORMKxHCbc4O', // ilovesakurako
- profile: {},
- settings: {},
- clientSettings: {}
- }
- }, opts));
-}
-
-function insertDriveFile(opts?) {
- return db.get('driveFiles.files').insert({
- length: opts.datasize,
- filename: 'strawberry-pasta.png',
- metadata: opts
- });
-}
-
-function insertDriveFolder(opts?) {
- return db.get('driveFolders').insert(merge({
- name: 'my folder',
- parentId: null
- }, opts));
-}
-
-function insertApp(opts?) {
- return db.get('apps').insert(merge({
- name: 'my app',
- secret: 'mysecret'
- }, opts));
-}
diff --git a/test/text.ts b/test/text.ts
index 8ce55cd1bc..a64999fc0b 100644
--- a/test/text.ts
+++ b/test/text.ts
@@ -1,11 +1,7 @@
-/**
- * Text Tests!
- */
+import * as assert from 'assert';
-const assert = require('assert');
-
-const analyze = require('../built/text/parse').default;
-const syntaxhighlighter = require('../built/text/parse/core/syntax-highlighter').default;
+import analyze from '../src/text/parse';
+import syntaxhighlighter from '../src/text/parse/core/syntax-highlighter';
describe('Text', () => {
it('can be analyzed', () => {
diff --git a/webpack.config.ts b/webpack.config.ts
index b2f67c914f..3aeecbd8a7 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -110,14 +110,14 @@ const plugins = [
//#region i18n
langs.forEach(lang => {
Object.keys(entry).forEach(file => {
- let src = fs.readFileSync(`${__dirname}/built/client/assets/${file}.${version}.-.${isProduction ? 'min' : 'raw'}.js`, 'utf8');
+ let src = fs.readFileSync(`${__dirname}/built/client/assets/${file}.${version}.-.${isProduction ? 'min' : 'raw'}.js`, 'utf-8');
const i18nReplacer = new I18nReplacer(lang);
src = src.replace(i18nReplacer.pattern, i18nReplacer.replacement);
src = src.replace('%lang%', lang);
- fs.writeFileSync(`${__dirname}/built/client/assets/${file}.${version}.${lang}.${isProduction ? 'min' : 'raw'}.js`, src, 'utf8');
+ fs.writeFileSync(`${__dirname}/built/client/assets/${file}.${version}.${lang}.${isProduction ? 'min' : 'raw'}.js`, src, 'utf-8');
});
});
//#endregion
@@ -146,27 +146,20 @@ module.exports = {
}, {
loader: 'replace',
query: {
- search: /%base64:(.+?)%/g.toString(),
- replace: 'base64replacement'
- }
- }, {
- loader: 'replace',
- query: {
- search: i18nPattern.toString(),
- replace: 'i18nReplacement',
- i18n: true
- }
- }, {
- loader: 'replace',
- query: {
- search: faPattern.toString(),
- replace: 'faReplacement'
- }
- }, {
- loader: 'replace',
- query: {
- search: /^<template>([\s\S]+?)\r?\n<\/template>/.toString(),
- replace: 'collapseSpacesReplacement'
+ qs: [{
+ search: /%base64:(.+?)%/g.toString(),
+ replace: 'base64replacement'
+ }, {
+ search: i18nPattern.toString(),
+ replace: 'i18nReplacement',
+ i18n: true
+ }, {
+ search: faPattern.toString(),
+ replace: 'faReplacement'
+ }, {
+ search: /^<template>([\s\S]+?)\r?\n<\/template>/.toString(),
+ replace: 'collapseSpacesReplacement'
+ }]
}
}]
}, {
diff --git a/webpack/loaders/replace.js b/webpack/loaders/replace.js
index 0326dcdab3..fd6bb3617b 100644
--- a/webpack/loaders/replace.js
+++ b/webpack/loaders/replace.js
@@ -1,22 +1,30 @@
-const loaderUtils = require('loader-utils');
+import { getOptions } from 'loader-utils';
function trim(text, g) {
return text.substring(1, text.length - (g ? 2 : 0));
}
-module.exports = function(src) {
+export default function(src) {
+ const fn = options => {
+ const search = options.search;
+ const g = search[search.length - 1] == 'g';
+ const file = this.resourcePath.replace(/\\/g, '/');
+ const replace = options.i18n ? global[options.replace].bind(null, {
+ src: file,
+ lang: options.lang
+ }) : global[options.replace];
+ if (typeof search != 'string' || search.length == 0) console.error('invalid search');
+ if (typeof replace != 'function') console.error('invalid replacer:', replace, this.request);
+ src = src.replace(new RegExp(trim(search, g), g ? 'g' : ''), replace);
+ };
+
this.cacheable();
- const options = loaderUtils.getOptions(this);
- const search = options.search;
- const g = search[search.length - 1] == 'g';
- const file = this.resourcePath.replace(/\\/g, '/');
- const replace = options.i18n ? global[options.replace].bind(null, {
- src: file,
- lang: options.lang
- }) : global[options.replace];
- if (typeof search != 'string' || search.length == 0) console.error('invalid search');
- if (typeof replace != 'function') console.error('invalid replacer:', replace, this.request);
- src = src.replace(new RegExp(trim(search, g), g ? 'g' : ''), replace);
+ const options = getOptions(this);
+ if (options.qs) {
+ options.qs.forEach(q => fn(q));
+ } else {
+ fn(options);
+ }
this.callback(null, src);
return src;
-};
+}