diff --git a/.eslintrc b/.eslintrc index 0943cb4b64..dc1426eb1f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -22,6 +22,7 @@ "globals": { "ENV": true, "VERSION": true, - "API": true + "API": true, + "LANGS": true } } diff --git a/.gitignore b/.gitignore index 53808240b7..41fef982c4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /build /built /data +/.cache-loader npm-debug.log *.pem run.bat diff --git a/.travis.yml b/.travis.yml index c86b737d21..f52fe7e3f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,10 @@ notifications: email: false +branches: + except: + - l10n_master + language: node_js node_js: diff --git a/assets/title-dark.svg b/assets/title-dark.svg new file mode 100644 index 0000000000..10139024ad --- /dev/null +++ b/assets/title-dark.svg @@ -0,0 +1,140 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="614.71039" + height="205.08009" + viewBox="0 0 162.64213 54.260776" + version="1.1" + id="svg8" + inkscape:version="0.92.1 r15371" + sodipodi:docname="misskey.svg" + inkscape:export-filename="C:\Users\Takumiya_Cho\Desktop\misskey.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96"> + <defs + id="defs2"> + <inkscape:path-effect + effect="simplify" + id="path-effect5115" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + <inkscape:path-effect + effect="simplify" + id="path-effect5104" + is_visible="true" + steps="1" + threshold="0.000408163" + smooth_angles="360" + helper_size="0" + simplify_individual_paths="false" + simplify_just_coalesce="false" + simplifyindividualpaths="false" + simplifyJustCoalesce="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.9899495" + inkscape:cx="370.82839" + inkscape:cy="79.043895" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:snap-bbox="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="false" + inkscape:snap-smooth-nodes="true" + inkscape:snap-center="true" + inkscape:snap-page="true" + inkscape:window-width="1920" + inkscape:window-height="1017" + inkscape:window-x="-8" + inkscape:window-y="1072" + inkscape:window-maximized="1" + inkscape:object-paths="true" + inkscape:bbox-paths="true" + fit-margin-top="50" + fit-margin-left="50" + fit-margin-bottom="20" + fit-margin-right="50" /> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="レイヤー 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-11.097531,-173.29664)"> + <g + transform="matrix(0.28612302,0,0,0.28612302,17.176981,141.74334)" + id="text4489-6" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.92471898px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + aria-label="Mi"> + <path + sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz" + inkscape:connector-curvature="0" + id="path5210" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#fff;fill-opacity:1;stroke-width:0.92471898px" + d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" /> + <path + inkscape:connector-curvature="0" + id="path5212" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#fff;fill-opacity:1;stroke-width:0.92471898px" + d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" /> + </g> + <path + inkscape:connector-curvature="0" + id="path5199" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 72.022691,200.53715 q 0.968125,0.24203 2.420312,0.5244 2.420313,0.40339 3.791824,1.29083 2.581666,1.69422 2.581666,5.08266 0,2.74302 -1.815234,4.47758 -2.097604,2.01693 -5.849089,2.01693 -2.743021,0 -6.131458,-0.76644 -1.089141,-0.24203 -1.774896,-1.08914 -0.645417,-0.84711 -0.645417,-1.89591 0,-1.29083 0.887448,-2.17828 0.927786,-0.92779 2.178281,-0.92779 0.363047,0 0.685756,0.0807 1.169817,0.24203 4.477578,0.60508 0.443724,0 0.968125,-0.0403 0.201693,0 0.201693,-0.24203 0.04034,-0.20169 -0.242032,-0.28237 -1.37151,-0.24203 -2.541328,-0.5244 -1.331172,-0.28237 -1.895911,-0.48406 -1.12948,-0.32271 -1.895912,-0.84711 -2.581667,-1.69422 -2.622005,-5.08266 0,-2.70268 1.855573,-4.47758 2.258958,-2.17828 6.413828,-1.97659 2.783359,0.12102 5.566719,0.7261 1.048802,0.24203 1.734557,1.08914 0.685756,0.84711 0.685756,1.93625 0,1.25049 -0.927787,2.17828 -0.887448,0.88745 -2.178281,0.88745 -0.322709,0 -0.645417,-0.0807 -1.169818,-0.24203 -4.517917,-0.56474 -0.403385,-0.0403 -0.766432,0 -0.322708,0.0403 -0.322708,0.24203 0.04034,0.24203 0.322708,0.32271 z" /> + <path + inkscape:connector-curvature="0" + id="path5201" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 89.577027,200.53715 q 0.968125,0.24203 2.420312,0.5244 2.420313,0.40339 3.791823,1.29083 2.581667,1.69422 2.581667,5.08266 0,2.74302 -1.815234,4.47758 -2.097604,2.01693 -5.849089,2.01693 -2.743021,0 -6.131458,-0.76644 -1.089141,-0.24203 -1.774896,-1.08914 -0.645417,-0.84711 -0.645417,-1.89591 0,-1.29083 0.887448,-2.17828 0.927786,-0.92779 2.178281,-0.92779 0.363047,0 0.685755,0.0807 1.169818,0.24203 4.477579,0.60508 0.443724,0 0.968125,-0.0403 0.201692,0 0.201692,-0.24203 0.04034,-0.20169 -0.242031,-0.28237 -1.37151,-0.24203 -2.541328,-0.5244 -1.331172,-0.28237 -1.895912,-0.48406 -1.129479,-0.32271 -1.895911,-0.84711 -2.581667,-1.69422 -2.622005,-5.08266 0,-2.70268 1.855573,-4.47758 2.258958,-2.17828 6.413828,-1.97659 2.783359,0.12102 5.566719,0.7261 1.048802,0.24203 1.734557,1.08914 0.685755,0.84711 0.685755,1.93625 0,1.25049 -0.927786,2.17828 -0.887448,0.88745 -2.178281,0.88745 -0.322709,0 -0.645417,-0.0807 -1.169818,-0.24203 -4.517917,-0.56474 -0.403385,-0.0403 -0.766432,0 -0.322708,0.0403 -0.322708,0.24203 0.04034,0.24203 0.322708,0.32271 z" /> + <path + inkscape:connector-curvature="0" + id="path5203" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 115.65209,203.87137 q 0.12101,0.0807 2.86404,2.78336 1.25049,1.21016 1.25049,2.94471 0,1.61354 -1.16982,2.86404 -1.16982,1.21016 -2.90437,1.21016 -1.65388,0 -2.86404,-1.16982 l -4.03385,-3.91284 q -0.16136,-0.12102 -0.32271,-0.12102 -0.32271,0 -0.32271,1.21016 0,1.69422 -1.21016,2.90438 -1.21015,1.16981 -2.90437,1.16981 -1.69422,0 -2.90438,-1.16981 -1.169807,-1.21016 -1.169807,-2.90438 v -18.79776 q 0,-1.69422 1.169807,-2.86404 1.21016,-1.21015 2.90438,-1.21015 1.69422,0 2.90437,1.21015 1.21016,1.16982 1.21016,2.86404 v 6.29281 q 0,0.40339 0.28237,0.5244 0.24203,0.12102 0.5244,-0.0807 0.16135,-0.0807 4.84063,-3.18675 1.0488,-0.64542 2.25895,-0.64542 2.21862,0 3.42878,1.81524 0.64542,1.0488 0.64542,2.25896 0,2.21862 -1.81524,3.42877 l -2.54133,1.61354 v 0.0403 l -0.0807,0.0403 q -0.56474,0.36305 -0.0403,0.88745 z" /> + <path + inkscape:connector-curvature="0" + id="path5205" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 131.25181,213.92955 q -4.19521,0 -7.18026,-2.94472 -2.94472,-2.98505 -2.94472,-7.18026 0,-4.15487 2.94472,-7.09958 2.98505,-2.98505 7.18026,-2.98505 4.15487,0 6.97857,2.78335 0.92778,0.92779 0.92778,2.25896 0,1.33118 -0.92778,2.25896 l -4.67928,4.63893 q -1.00846,1.00847 -2.01692,1.00847 -1.45219,0 -2.25896,-0.80677 -0.80677,-0.80677 -0.80677,-2.13795 0,-1.29083 0.92778,-2.21862 l 0.80678,-0.84711 q 0.16135,-0.12101 0.0807,-0.24203 -0.12101,-0.0807 -0.32271,-0.0403 -0.80677,0.20169 -1.37151,0.80677 -1.12948,1.08914 -1.12948,2.622 0,1.5732 1.08915,2.70268 1.12947,1.08914 2.70268,1.08914 1.53286,0 2.622,-1.12947 0.92779,-0.92779 2.25896,-0.92779 1.33117,0 2.25896,0.92779 0.92779,0.92778 0.92779,2.25895 0,1.33118 -0.92779,2.25896 -2.98505,2.94472 -7.13992,2.94472 z" /> + <path + inkscape:connector-curvature="0" + id="path5207" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 160.51049,198.1433 v 5.60705 q 0,0.56474 -0.0807,1.21016 v 7.38195 q 0,4.51792 -2.74302,7.2206 -2.70268,2.70269 -7.30128,2.70269 -2.66234,0 -4.80028,-1.00847 -2.13795,-0.96812 -2.13795,-3.3481 0,-0.80677 0.36305,-1.53286 0.96812,-2.17828 3.3481,-2.17828 0.56474,0 1.5732,0.32271 1.00847,0.3227 1.65388,0.3227 1.69422,0 2.21862,-0.72609 0.20169,-0.28237 0.0807,-0.44372 -0.16136,-0.24204 -0.56474,-0.16136 -0.68576,0.12102 -1.49253,0.12102 -4.07419,0 -6.97856,-2.90438 -2.90438,-2.90437 -2.90438,-6.97857 v -5.60705 q 0,-1.69422 1.16982,-2.86404 1.21015,-1.21016 2.90437,-1.21016 1.69422,0 2.90438,1.21016 1.21015,1.16982 1.21015,2.86404 v 5.60705 q 0,0.68576 0.48407,1.21016 0.5244,0.48406 1.21015,0.48406 0.7261,0 1.21016,-0.48406 0.48406,-0.5244 0.48406,-1.21016 v -5.60705 q 0,-1.69422 1.21016,-2.86404 1.21015,-1.21016 2.90437,-1.21016 1.69422,0 2.86404,1.21016 1.21016,1.16982 1.21016,2.86404 z" /> + </g> +</svg> diff --git a/cli/clean-cached-remote-files.js b/cli/clean-cached-remote-files.js index e4db37ef97..a9c38a4cdf 100644 --- a/cli/clean-cached-remote-files.js +++ b/cli/clean-cached-remote-files.js @@ -8,7 +8,8 @@ const { default: User } = require('../built/models/user'); const q = { 'metadata._user.host': { $ne: null - } + }, + 'metadata.isMetaOnly': false }; async function main() { @@ -56,8 +57,7 @@ async function main() { DriveFile.update({ _id: file._id }, { $set: { - 'metadata.deletedAt': new Date(), - 'metadata.isExpired': true + 'metadata.isMetaOnly': true } }) ]).then(async () => { diff --git a/docs/setup.en.md b/docs/setup.en.md index b858a4a2a4..8dde4d00d6 100644 --- a/docs/setup.en.md +++ b/docs/setup.en.md @@ -43,13 +43,7 @@ Please install and setup these softwares: *4.* Prepare configuration ---------------------------------------------------------------- -1. Copy `example.yml` of `.config` directory -2. Rename it to `default.yml` -3. Edit it - ---- - -Or you can generate config file via `npm run config` command. +You need to generate config file via `npm run config` command. *5.* Build Misskey ---------------------------------------------------------------- diff --git a/docs/setup.ja.md b/docs/setup.ja.md index c45ebcdca0..0f1e46761b 100644 --- a/docs/setup.ja.md +++ b/docs/setup.ja.md @@ -43,18 +43,14 @@ web-push generate-vapid-keys *4.* 設定ファイルを用意する ---------------------------------------------------------------- -1. `.config`ディレクトリ内の`example.yml`をコピー -2. `default.yml`にリネーム -3. 編集する - ---- - -または、`npm run config`コマンドを利用して、ガイドに従って情報を -入力して設定ファイルを生成することもできます。 +`npm run config`コマンドを利用して、ガイドに従って情報を入力してください。 *5.* Misskeyのビルド ---------------------------------------------------------------- -1. `npm run build` +1. `npm install -g node-gyp` +2. `node-gyp configure` +3. `node-gyp build` +4. `npm run build` *6.* 以上です! ---------------------------------------------------------------- @@ -78,4 +74,4 @@ VPSなどでビルドする時は、もしかしたらメモリが足りなく 3. npm run webpack 4. built/client をサーバーにアップロードする 5. サーバー上で、npm run gulp -6. 完了 \ No newline at end of file +6. 完了 diff --git a/locales/de.yml b/locales/de.yml index 7d0ffe4084..5395de73ab 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -1,7 +1,7 @@ --- meta: lang: "Deutsch" - divider: "" + divider: " " common: misskey: "Teile alles mit anderen mithilfe von Misskey" time: diff --git a/locales/en.yml b/locales/en.yml index adaf433dd2..04f54957e1 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -359,7 +359,7 @@ desktop/views/components/renote-form.vue: desktop/views/components/renote-form-window.vue: title: "Are you sure you want to renote this note?" desktop/views/components/settings-window.vue: - settings: "設定" + settings: "Settings" desktop/views/components/settings.vue: profile: "Profile" notification: "Notification" diff --git a/locales/fr.yml b/locales/fr.yml index 9ed78f6ec2..4a9ddd380e 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -1,7 +1,7 @@ --- meta: - lang: "日本語" - divider: "" + lang: "Français" + divider: " " common: misskey: "Partagez avec les autres en utilisant Misskey" time: diff --git a/locales/ja.yml b/locales/ja.yml index c3ee3e6c9f..0fcbca5361 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -173,6 +173,16 @@ common/views/components/twitter-setting.vue: common/views/components/uploader.vue: waiting: "待機中" +common/views/components/visibility-chooser.vue: + public: "公開" + home: "ホーム" + home-desc: "ホームタイムラインにのみ公開" + followers: "フォロワー" + followers-desc: "自分のフォロワーにのみ公開" + specified: "ダイレクト" + specified-desc: "指定したユーザーにのみ公開" + private: "非公開" + common/views/widgets/broadcast.vue: fetching: "確認中" no-broadcasts: "お知らせはありません" @@ -340,6 +350,14 @@ desktop/views/components/messaging-room-window.vue: desktop/views/components/messaging-window.vue: title: "メッセージ" +desktop/views/components/note-detail.vue: + more: "会話をもっと読み込む" + private: "(この投稿は非公開です)" + reposted-by: "{}がRenote" + location: "位置情報" + renote: "Renote" + add-reaction: "リアクション" + desktop/views/components/note-detail.sub.vue: private: "(この投稿は非公開です)" @@ -399,6 +417,9 @@ desktop/views/components/renote-form.vue: desktop/views/components/renote-form-window.vue: title: "この投稿をRenoteしますか?" +desktop/views/components/settings-window.vue: + settings: "設定" + desktop/views/components/settings.vue: profile: "プロフィール" notification: "通知" @@ -477,9 +498,6 @@ desktop/views/components/settings.vue: advanced-settings: "高度な設定" debug-mode: "デバッグモードを有効にする" debug-mode-desc: "この設定はブラウザに記憶されます。" - use-raw-script: "生のスクリプトを読み込む" - use-raw-script-desc: "圧縮されていない「生の」スクリプトを使用します。サイズが大きいため、読み込みに時間がかかる場合があります。この設定はブラウザに記憶されます。" - source-info: "Misskeyはソースマップも提供しています。" experimental: "実験的機能を有効にする" experimental-desc: "実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。この設定はブラウザに記憶されます。" tools: "ツール" @@ -535,6 +553,13 @@ desktop/views/components/settings.profile.vue: description: "自己紹介" birthday: "誕生日" save: "保存" + is-bot: "このアカウントはBotです" + is-cat: "このアカウントはCatです" + +desktop/views/components/sub-note-content.vue: + hidden: "(この投稿は非公開です)" + media: "つのメディア" + poll: "投票" desktop/views/components/taskmanager.vue: title: "タスクマネージャ" @@ -583,6 +608,29 @@ desktop/views/components/users-list.vue: load-more: "もっと" fetching: "読み込んでいます" +desktop/views/components/users-list-item.vue: + followed: "フォローされています" + +desktop/views/components/window.vue: + popout: "ポップアウト" + close: "閉じる" + +desktop/views/pages/welcome.vue: + signin: "ログイン" + signup: "新規登録" + signin-button: "やってる" + signup-button: "やる" + timeline: "タイムライン" + +desktop/views/pages/drive.vue: + title: "Misskey Drive" + +desktop/views/pages/favorites.vue: + more: "さらに読み込む" + +desktop/views/pages/home-customize.vue: + title: "ホームのカスタマイズ" + desktop/views/pages/note.vue: prev: "前の投稿" next: "次の投稿" @@ -593,6 +641,11 @@ desktop/views/pages/selectdrive.vue: cancel: "キャンセル" upload: "PCからドライブにファイルをアップロード" +desktop/views/pages/user-list.users.vue: + users: "ユーザー" + add-user: "ユーザーを追加" + username: "ユーザー名" + desktop/views/pages/user/user.followers-you-know.vue: title: "知り合いのフォロワー" loading: "読み込み中" @@ -625,6 +678,12 @@ desktop/views/pages/user/user.profile.vue: muted: "ミュートしています" unmute: "ミュート解除" +desktop/views/pages/user/user.timeline.vue: + default: "投稿" + with-replies: "投稿と返信" + with-media: "メディア" + empty: "このユーザーはまだ何も投稿していないようです。" + desktop/views/widgets/messaging.vue: title: "メッセージ" @@ -642,6 +701,10 @@ desktop/views/widgets/post-form.vue: note: "投稿" placeholder: "いまどうしてる?" +desktop/views/widgets/profile.vue: + update-banner: "クリックでバナー編集" + update-avatar: "クリックでアバター編集" + desktop/views/widgets/trends.vue: title: "トレンド" refresh: "他を見る" @@ -735,7 +798,9 @@ mobile/views/pages/following.vue: following-of: "{}のフォロー" mobile/views/pages/home.vue: - timeline: "タイムライン" + home: "ホーム" + local: "ローカル" + global: "グローバル" mobile/views/pages/messaging.vue: messaging: "メッセージ" @@ -753,20 +818,19 @@ mobile/views/pages/notifications.vue: read-all: "すべての通知を既読にしますか?" mobile/views/pages/settings/settings.profile.vue: - title: "プロフィール設定" - will-be-published: "これらのプロフィールは公開されます。" + title: "プロフィール" name: "名前" + account: "アカウント" location: "場所" description: "自己紹介" birthday: "誕生日" avatar: "アイコン" banner: "バナー" - avatar-saved: "アイコンを保存しました" - banner-saved: "バナーを保存しました" - set-avatar: "アイコンを選択する" - set-banner: "バナーを選択する" + is-cat: "このアカウントはCatです" save: "保存" saved: "プロフィールを保存しました" + uploading: "アップロード中" + upload-failed: "アップロードに失敗しました" mobile/views/pages/search.vue: search: "検索" @@ -777,9 +841,40 @@ mobile/views/pages/selectdrive.vue: mobile/views/pages/settings.vue: signed-in-as: "{}としてサインイン中" - profile: "プロフィール" + lang: "言語" + lang-tip: "変更はページの再読み込み後に反映されます。" + recommended: "推奨" + auto: "自動" + specify-language: "言語を指定" + design: "デザインと表示" + dark-mode: "ダークモード" + i-am-under-limited-internet: "私は通信を制限されている" + circle-icons: "円形のアイコンを使用" + timeline: "タイムライン" + show-reply-target: "リプライ先を表示する" + show-my-renotes: "自分の行ったRenoteを表示する" + show-renoted-my-notes: "Renoteされた自分の投稿を表示する" + post-style: "投稿の表示スタイル" + post-style-standard: "標準" + post-style-smart: "スマート" + behavior: "動作" + fetch-on-scroll: "スクロールで自動読み込み" + disable-via-mobile: "「モバイルからの投稿」フラグを付けない" + load-raw-images: "添付された画像を高画質で表示する" + load-remote-media: "リモートサーバーのメディアを表示する" twitter: "Twitter連携" - signin-history: "サインイン履歴" + twitter-connect: "Twitterアカウントに接続する" + twitter-reconnect: "再接続する" + twitter-disconnect: "切断する" + update: "Misskey Update" + version: "バージョン:" + latest-version: "最新のバージョン:" + update-checking: "アップデートを確認中" + check-for-updates: "アップデートを確認" + no-updates: "利用可能な更新はありません" + no-updates-desc: "お使いのMisskeyは最新です。" + update-available: "新しいバージョンが利用可能です" + update-available-desc: "ページを再度読み込みすると更新が適用されます。" settings: "設定" signout: "サインアウト" diff --git a/locales/pl.yml b/locales/pl.yml index b6427bdf09..9324704bb5 100644 --- a/locales/pl.yml +++ b/locales/pl.yml @@ -1,7 +1,7 @@ --- meta: - lang: "japoński" - divider: "" + lang: "język polski" + divider: " " common: misskey: "Dziel się zawartością z innymi korzystając z Misskey." time: diff --git a/locales/ru.yml b/locales/ru.yml index 08551f2db5..290c660ff8 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -1,7 +1,7 @@ --- meta: - lang: "日本語" - divider: "" + lang: "Русский язык" + divider: " " common: misskey: "Misskeyで皆と共有しよう。" time: diff --git a/package.json b/package.json index 3fa261b8a1..534b0c296d 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "misskey", "author": "syuilo <i@syuilo.com>", - "version": "2.10.1", - "clientVersion": "1.0.5407", + "version": "2.17.0", + "clientVersion": "1.0.5731", "codename": "nighthike", "main": "./built/index.js", "private": true, @@ -65,7 +65,7 @@ "@types/mongodb": "3.0.18", "@types/monk": "6.0.0", "@types/ms": "0.7.30", - "@types/node": "10.1.0", + "@types/node": "10.1.2", "@types/nopt": "3.0.29", "@types/parse5": "3.0.0", "@types/pug": "2.0.4", @@ -80,7 +80,7 @@ "@types/speakeasy": "2.0.2", "@types/tmp": "0.0.33", "@types/uuid": "3.4.3", - "@types/webpack": "4.1.7", + "@types/webpack": "4.4.0", "@types/webpack-stream": "3.2.10", "@types/websocket": "0.0.39", "@types/ws": "5.1.1", @@ -98,8 +98,8 @@ "deepcopy": "0.6.3", "diskusage": "0.2.4", "dompurify": "1.0.4", - "elasticsearch": "14.2.2", - "element-ui": "2.3.8", + "elasticsearch": "15.0.0", + "element-ui": "2.3.9", "emojilib": "2.2.12", "escape-regexp": "0.0.1", "eslint": "4.19.1", @@ -124,7 +124,7 @@ "gulp-typescript": "4.0.2", "gulp-uglify": "3.0.0", "gulp-util": "3.0.8", - "hard-source-webpack-plugin": "0.6.7", + "hard-source-webpack-plugin": "0.6.9", "highlight.js": "9.12.0", "html-minifier": "3.5.15", "http-signature": "1.2.0", @@ -146,11 +146,11 @@ "koa-slow": "2.1.0", "koa-views": "6.1.4", "kue": "0.11.6", - "license-checker": "19.0.0", + "license-checker": "20.0.0", "loader-utils": "1.1.0", "mecab-async": "0.1.2", "mkdirp": "0.5.1", - "mocha": "5.1.1", + "mocha": "5.2.0", "moji": "0.5.1", "mongodb": "3.0.8", "monk": "6.0.6", @@ -205,12 +205,13 @@ "vue-cropperjs": "2.2.0", "vue-js-modal": "1.3.13", "vue-json-tree-view": "2.1.4", - "vue-loader": "15.0.11", + "vue-loader": "15.1.0", "vue-material": "^1.0.0-beta-10.2", "vue-router": "3.0.1", "vue-template-compiler": "2.5.16", "vuedraggable": "2.16.0", "vuex": "3.0.1", + "vuex-persistedstate": "^2.5.4", "web-push": "3.3.1", "webfinger.js": "2.6.6", "webpack": "4.8.3", diff --git a/src/build/i18n.ts b/src/build/i18n.ts index addc35ce59..35854055d0 100644 --- a/src/build/i18n.ts +++ b/src/build/i18n.ts @@ -7,7 +7,7 @@ import locale from '../../locales'; export default class Replacer { private lang: string; - public pattern = /%i18n:([a-z0-9_\-\.\/\|\!]+?)%/g; + public pattern = /%i18n:([a-z0-9_\-\.\/\|]+?)%/g; constructor(lang: string) { this.lang = lang; @@ -56,11 +56,6 @@ export default class Replacer { public replacement(match, key) { let path = null; - const shouldEscape = key[0] == '!'; - if (shouldEscape) { - key = key.substr(1); - } - if (key.indexOf('|') != -1) { path = key.split('|')[0]; key = key.split('|')[1]; @@ -68,8 +63,6 @@ export default class Replacer { const txt = this.get(path, key); - return shouldEscape - ? txt.replace(/'/g, '\\x27').replace(/"/g, '\\x22') - : txt.replace(/"/g, '"'); + return txt.replace(/'/g, '\\x27').replace(/"/g, '\\x22'); } } diff --git a/src/client/app/app.styl b/src/client/app/app.styl index 431b9daa65..ba694b73ae 100644 --- a/src/client/app/app.styl +++ b/src/client/app/app.styl @@ -7,6 +7,11 @@ html cursor progress !important body + // for md + font-size 16px !important + line-height initial !important + letter-spacing initial !important + overflow-wrap break-word #error diff --git a/src/client/app/boot.js b/src/client/app/boot.js index 9338bc501e..7b884c8a54 100644 --- a/src/client/app/boot.js +++ b/src/client/app/boot.js @@ -18,6 +18,14 @@ return; } + //#region Load settings + let settings = null; + const vuex = localStorage.getItem('vuex'); + if (vuex) { + settings = JSON.parse(vuex); + } + //#endregion + // Get the current url information const url = new URL(location.href); @@ -29,11 +37,16 @@ if (url.pathname == '/auth') app = 'auth'; //#endregion - // Detect the user language - // Note: The default language is Japanese + //#region Detect the user language let lang = navigator.language.split('-')[0]; + + // The default language is English if (!LANGS.includes(lang)) lang = 'en'; - if (localStorage.getItem('lang')) lang = localStorage.getItem('lang'); + + if (settings) { + if (settings.device.lang) lang = settings.device.lang; + } + //#endregion // Detect the user agent const ua = navigator.userAgent.toLowerCase(); @@ -61,20 +74,15 @@ } // Dark/Light - if (localStorage.getItem('darkmode') == 'true') { - document.documentElement.setAttribute('data-darkmode', 'true'); + if (settings) { + if (settings.device.darkmode) { + document.documentElement.setAttribute('data-darkmode', 'true'); + } } // Script version const ver = localStorage.getItem('v') || VERSION; - // Whether in debug mode - const isDebug = localStorage.getItem('debug') == 'true'; - - // Whether use raw version script - const raw = (localStorage.getItem('useRawScript') == 'true' && isDebug) - || ENV != 'production'; - // Get salt query const salt = localStorage.getItem('salt') ? '?salt=' + localStorage.getItem('salt') @@ -84,7 +92,7 @@ // Note: 'async' make it possible to load the script asyncly. // 'defer' make it possible to run the script when the dom loaded. const script = document.createElement('script'); - script.setAttribute('src', `/assets/${app}.${ver}.${lang}.${raw ? 'raw' : 'min'}.js${salt}`); + script.setAttribute('src', `/assets/${app}.${ver}.${lang}.js${salt}`); script.setAttribute('async', 'true'); script.setAttribute('defer', 'true'); head.appendChild(script); diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts index 1e303017eb..b5ba6916d1 100644 --- a/src/client/app/common/scripts/check-for-update.ts +++ b/src/client/app/common/scripts/check-for-update.ts @@ -23,7 +23,7 @@ export default async function(mios: MiOS, force = false, silent = false) { } if (!silent) { - alert('%i18n:!common.update-available%'.replace('{newer}', newer).replace('{current}', current)); + alert('%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current)); } return newer; diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts index 09d830bece..44d07e331a 100644 --- a/src/client/app/common/scripts/streaming/home.ts +++ b/src/client/app/common/scripts/streaming/home.ts @@ -62,7 +62,7 @@ export class HomeStream extends Stream { // トークンが再生成されたとき // このままではMisskeyが利用できないので強制的にサインアウトさせる this.on('my_token_regenerated', () => { - alert('%i18n:!common.my-token-regenerated%'); + alert('%i18n:common.my-token-regenerated%'); os.signout(); }); } diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue index 8ec359e83c..3e1b17635f 100644 --- a/src/client/app/common/views/components/avatar.vue +++ b/src/client/app/common/views/components/avatar.vue @@ -21,10 +21,17 @@ export default Vue.extend({ } }, computed: { + lightmode(): boolean { + return this.$store.state.device.lightmode; + }, style(): any { return { - backgroundColor: this.user.avatarColor && this.user.avatarColor.length == 3 ? `rgb(${ this.user.avatarColor.join(',') })` : null, - backgroundImage: `url(${ this.user.avatarUrl }?thumbnail)`, + backgroundColor: this.lightmode + ? `rgb(${ this.user.avatarColor.slice(0, 3).join(',') })` + : this.user.avatarColor && this.user.avatarColor.length == 3 + ? `rgb(${ this.user.avatarColor.join(',') })` + : null, + backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl }?thumbnail)`, borderRadius: (this as any).clientSettings.circleIcons ? '100%' : null }; } diff --git a/src/client/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue index 6a922676b7..6c23cc7969 100644 --- a/src/client/app/common/views/components/connect-failed.troubleshooter.vue +++ b/src/client/app/common/views/components/connect-failed.troubleshooter.vue @@ -8,21 +8,21 @@ <template v-if="network">%fa:check%</template> <template v-if="!network">%fa:times%</template> </template> - {{ network == null ? '%i18n:!@checking-network%' : '%i18n:!@network%' }}<mk-ellipsis v-if="network == null"/> + {{ network == null ? '%i18n:@checking-network%' : '%i18n:@network%' }}<mk-ellipsis v-if="network == null"/> </p> <p v-if="network == true" :data-wip="internet == null"> <template v-if="internet != null"> <template v-if="internet">%fa:check%</template> <template v-if="!internet">%fa:times%</template> </template> - {{ internet == null ? '%i18n:!@checking-internet%' : '%i18n:!@internet%' }}<mk-ellipsis v-if="internet == null"/> + {{ internet == null ? '%i18n:@checking-internet%' : '%i18n:@internet%' }}<mk-ellipsis v-if="internet == null"/> </p> <p v-if="internet == true" :data-wip="server == null"> <template v-if="server != null"> <template v-if="server">%fa:check%</template> <template v-if="!server">%fa:times%</template> </template> - {{ server == null ? '%i18n:!@checking-server%' : '%i18n:!@server%' }}<mk-ellipsis v-if="server == null"/> + {{ server == null ? '%i18n:@checking-server%' : '%i18n:@server%' }}<mk-ellipsis v-if="server == null"/> </p> </div> <p v-if="!end">%i18n:@finding%<mk-ellipsis/></p> diff --git a/src/client/app/common/views/components/connect-failed.vue b/src/client/app/common/views/components/connect-failed.vue index 6c194ff982..0f686926b0 100644 --- a/src/client/app/common/views/components/connect-failed.vue +++ b/src/client/app/common/views/components/connect-failed.vue @@ -3,9 +3,9 @@ <img src="data:image/jpeg;base64,%base64:/assets/error.jpg%" alt=""/> <h1>%i18n:@title%</h1> <p class="text"> - <span>{{ '%i18n:!@description%'.substr(0, '%i18n:!@description%'.indexOf('{')) }}</span> - <a @click="reload">{{ '%i18n:!@description%'.match(/\{(.+?)\}/)[1] }}</a> - <span>{{ '%i18n:!@description%'.substr('%i18n:!@description%'.indexOf('}') + 1) }}</span> + <span>{{ '%i18n:@description%'.substr(0, '%i18n:@description%'.indexOf('{')) }}</span> + <a @click="reload">{{ '%i18n:@description%'.match(/\{(.+?)\}/)[1] }}</a> + <span>{{ '%i18n:@description%'.substr('%i18n:@description%'.indexOf('}') + 1) }}</span> </p> <button v-if="!troubleshooting" @click="troubleshooting = true">%i18n:@troubleshoot%</button> <x-troubleshooter v-if="troubleshooting"/> @@ -28,7 +28,7 @@ export default Vue.extend({ }, mounted() { document.title = 'Oops!'; - document.documentElement.style.background = '#f8f8f8'; + document.documentElement.style.setProperty('background', '#f8f8f8', 'important'); }, methods: { reload() { diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue index 32a43ace57..050906cf44 100644 --- a/src/client/app/common/views/components/messaging-room.form.vue +++ b/src/client/app/common/views/components/messaging-room.form.vue @@ -197,7 +197,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-messaging-form +root(isDark) > textarea cursor auto display block @@ -209,10 +209,10 @@ export default Vue.extend({ padding 8px resize none font-size 1em - color #000 + color isDark ? #fff : #000 outline none border none - border-top solid 1px #eee + border-top solid 1px isDark ? #4b5056 : #eee border-radius 0 box-shadow none background transparent @@ -302,4 +302,10 @@ export default Vue.extend({ input[type=file] display none +.mk-messaging-form[data-darkmode] + root(true) + +.mk-messaging-form:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue index ba0ab3209f..ef39199dc4 100644 --- a/src/client/app/common/views/components/messaging-room.message.vue +++ b/src/client/app/common/views/components/messaging-room.message.vue @@ -59,8 +59,10 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.message - $me-balloon-color = #23A7B6 +@import '~const.styl' + +root(isDark) + $me-balloon-color = $theme-color padding 10px 12px 10px 12px background-color transparent @@ -126,7 +128,7 @@ export default Vue.extend({ bottom -4px left -12px margin 0 - color rgba(#000, 0.5) + color isDark ? rgba(#fff, 0.5) : rgba(#000, 0.5) font-size 11px > .content @@ -187,7 +189,7 @@ export default Vue.extend({ display block margin 2px 0 0 0 font-size 10px - color rgba(#000, 0.4) + color isDark ? rgba(#fff, 0.4) : rgba(#000, 0.4) > [data-fa] margin-left 4px @@ -200,8 +202,9 @@ export default Vue.extend({ padding-left 66px > .balloon + $color = isDark ? #2d3338 : #eee float left - background #eee + background $color &[data-no-text] background transparent @@ -209,10 +212,15 @@ export default Vue.extend({ &:not([data-no-text]):before left -14px border-top solid 8px transparent - border-right solid 8px #eee + border-right solid 8px $color border-bottom solid 8px transparent border-left solid 8px transparent + > .content + > .text + if isDark + color #fff + > footer text-align left @@ -241,7 +249,7 @@ export default Vue.extend({ > .content > p.is-deleted - color rgba(255, 255, 255, 0.5) + color rgba(#fff, 0.5) > .text >>> &, * @@ -254,4 +262,10 @@ export default Vue.extend({ > .baloon opacity 0.5 +.message[data-darkmode] + root(true) + +.message:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue index a45114e6bb..79756b22eb 100644 --- a/src/client/app/common/views/components/messaging-room.vue +++ b/src/client/app/common/views/components/messaging-room.vue @@ -8,7 +8,7 @@ <p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:@empty%</p> <p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages">%fa:flag%%i18n:@no-history%</p> <button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages"> - <template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:!common.loading%' : '%i18n:!@more%' }} + <template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:@more%' }} </button> <template v-for="(message, i) in _messages"> <x-message :message="message" :key="message.id"/> @@ -18,7 +18,11 @@ </template> </div> <footer> - <div ref="notifications" class="notifications"></div> + <transition name="fade"> + <div class="new-message" v-show="showIndicator"> + <button @click="onIndicatorClick">%fa:arrow-circle-down%%i18n:@new-message%</button> + </div> + </transition> <x-form :user="user" ref="form"/> </footer> </div> @@ -45,7 +49,9 @@ export default Vue.extend({ fetchingMoreMessages: false, messages: [], existMoreMessages: false, - connection: null + connection: null, + showIndicator: false, + timer: null }; }, @@ -149,9 +155,9 @@ export default Vue.extend({ onMessage(message) { // サウンドを再生する - if ((this as any).os.isEnableSounds) { + if (this.$store.state.device.enableSounds) { const sound = new Audio(`${url}/assets/message.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; + sound.volume = this.$store.state.device.soundVolume; sound.play(); } @@ -172,7 +178,7 @@ export default Vue.extend({ }); } else if (message.userId != (this as any).os.i.id) { // Notify - this.notify('%i18n:!@new-message%'); + this.notifyNewMessage(); } }, @@ -205,18 +211,18 @@ export default Vue.extend({ } }, - notify(message) { - const n = document.createElement('p') as any; - n.innerHTML = '%fa:arrow-circle-down%' + message; - n.onclick = () => { - this.scrollToBottom(); - n.parentNode.removeChild(n); - }; - (this.$refs.notifications as any).appendChild(n); + onIndicatorClick() { + this.showIndicator = false; + this.scrollToBottom(); + }, - setTimeout(() => { - n.style.opacity = 0; - setTimeout(() => n.parentNode.removeChild(n), 1000); + notifyNewMessage() { + this.showIndicator = true; + + if (this.timer) clearTimeout(this.timer); + + this.timer = setTimeout(() => { + this.showIndicator = false; }, 4000); }, @@ -238,11 +244,12 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-messaging-room +root(isDark) display flex flex 1 flex-direction column height 100% + background isDark ? #191b22 : #fff > .stream width 100% @@ -256,7 +263,7 @@ export default Vue.extend({ padding 16px 8px 8px 8px text-align center font-size 0.8em - color rgba(#000, 0.4) + color rgba(isDark ? #fff : #000, 0.4) [data-fa] margin-right 4px @@ -267,7 +274,7 @@ export default Vue.extend({ padding 16px 8px 8px 8px text-align center font-size 0.8em - color rgba(#000, 0.4) + color rgba(isDark ? #fff : #000, 0.4) [data-fa] margin-right 4px @@ -278,7 +285,7 @@ export default Vue.extend({ padding 16px text-align center font-size 0.8em - color rgba(#000, 0.4) + color rgba(isDark ? #fff : #000, 0.4) [data-fa] margin-right 4px @@ -322,7 +329,7 @@ export default Vue.extend({ left 0 right 0 margin 0 auto - background rgba(#000, 0.1) + background rgba(isDark ? #fff : #000, 0.1) > span display inline-block @@ -330,8 +337,8 @@ export default Vue.extend({ padding 0 16px //font-weight bold line-height 32px - color rgba(#000, 0.3) - background #fff + color rgba(isDark ? #fff : #000, 0.3) + background isDark ? #191b22 : #fff > footer position -webkit-sticky @@ -342,30 +349,32 @@ export default Vue.extend({ max-width 600px margin 0 auto padding 0 - background rgba(255, 255, 255, 0.95) + background rgba(isDark ? #282c37 : #fff, 0.95) background-clip content-box - > .notifications + > .new-message position absolute top -48px width 100% padding 8px 0 text-align center - &:empty - display none - - > p + > button display inline-block margin 0 - padding 0 12px 0 28px + padding 0 12px 0 30px cursor pointer line-height 32px font-size 12px color $theme-color-foreground background $theme-color border-radius 16px - transition opacity 1s ease + + &:hover + background lighten($theme-color, 10%) + + &:active + background darken($theme-color, 10%) > [data-fa] position absolute @@ -374,4 +383,17 @@ export default Vue.extend({ line-height 32px font-size 16px +.fade-enter-active, .fade-leave-active + transition opacity 0.1s + +.fade-enter, .fade-leave-to + transition opacity 0.5s + opacity 0 + +.mk-messaging-room[data-darkmode] + root(true) + +.mk-messaging-room:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/common/views/components/othello.game.vue b/src/client/app/common/views/components/othello.game.vue index 8c646cce07..ea75558d10 100644 --- a/src/client/app/common/views/components/othello.game.vue +++ b/src/client/app/common/views/components/othello.game.vue @@ -162,9 +162,9 @@ export default Vue.extend({ this.o.put(this.myColor, pos); // サウンドを再生する - if ((this as any).os.isEnableSounds) { + if (this.$store.state.device.enableSounds) { const sound = new Audio(`${url}/assets/othello-put-me.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; + sound.volume = this.$store.state.device.soundVolume; sound.play(); } @@ -186,9 +186,9 @@ export default Vue.extend({ this.$forceUpdate(); // サウンドを再生する - if ((this as any).os.isEnableSounds && x.color != this.myColor) { + if (this.$store.state.device.enableSounds && x.color != this.myColor) { const sound = new Audio(`${url}/assets/othello-put-you.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; + sound.volume = this.$store.state.device.soundVolume; sound.play(); } }, diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue index 95bcba996e..115c934c8b 100644 --- a/src/client/app/common/views/components/poll-editor.vue +++ b/src/client/app/common/views/components/poll-editor.vue @@ -5,7 +5,7 @@ </p> <ul ref="choices"> <li v-for="(choice, i) in choices"> - <input :value="choice" @input="onInput(i, $event)" :placeholder="'%i18n:!@choice-n%'.replace('{}', i + 1)"> + <input :value="choice" @input="onInput(i, $event)" :placeholder="'%i18n:@choice-n%'.replace('{}', i + 1)"> <button @click="remove(i)" title="%i18n:@remove%"> %fa:times% </button> diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue index 46e41cbcdb..660247edbc 100644 --- a/src/client/app/common/views/components/poll.vue +++ b/src/client/app/common/views/components/poll.vue @@ -1,19 +1,19 @@ <template> <div class="mk-poll" :data-is-voted="isVoted"> <ul> - <li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? '%i18n:!@vote-to%'.replace('{}', choice.text) : ''"> + <li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? '%i18n:@vote-to%'.replace('{}', choice.text) : ''"> <div class="backdrop" :style="{ 'width': (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div> <span> <template v-if="choice.isVoted">%fa:check%</template> <span>{{ choice.text }}</span> - <span class="votes" v-if="showResult">({{ '%i18n:!@vote-count%'.replace('{}', choice.votes) }})</span> + <span class="votes" v-if="showResult">({{ '%i18n:@vote-count%'.replace('{}', choice.votes) }})</span> </span> </li> </ul> <p v-if="total > 0"> - <span>{{ '%i18n:!@total-users%'.replace('{}', total) }}</span> + <span>{{ '%i18n:@total-users%'.replace('{}', total) }}</span> <span>・</span> - <a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? '%i18n:!@vote%' : '%i18n:!@show-result%' }}</a> + <a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? '%i18n:@vote%' : '%i18n:@show-result%' }}</a> <span v-if="isVoted">%i18n:@voted%</span> </p> </div> diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue index e2c8a6ed3f..0db6f66b37 100644 --- a/src/client/app/common/views/components/reaction-picker.vue +++ b/src/client/app/common/views/components/reaction-picker.vue @@ -22,7 +22,7 @@ import Vue from 'vue'; import * as anime from 'animejs'; -const placeholder = '%i18n:!@choose-reaction%'; +const placeholder = '%i18n:@choose-reaction%'; export default Vue.extend({ props: ['note', 'source', 'compact', 'cb'], diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue index 7fb9fc3fd4..6b9d58e0a8 100644 --- a/src/client/app/common/views/components/signin.vue +++ b/src/client/app/common/views/components/signin.vue @@ -9,7 +9,7 @@ <label class="token" v-if="user && user.twoFactorEnabled"> <input v-model="token" type="number" placeholder="%i18n:@token%" required/>%fa:lock% </label> - <button type="submit" :disabled="signing">{{ signing ? '%i18n:!@signing-in%' : '%i18n:!@signin%' }}</button> + <button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</button> もしくは <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a> </form> </template> diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue index 516979acd0..f8bf7dd798 100644 --- a/src/client/app/common/views/components/signup.vue +++ b/src/client/app/common/views/components/signup.vue @@ -127,7 +127,7 @@ export default Vue.extend({ location.href = '/'; }); }).catch(() => { - alert('%i18n:!@some-error%'); + alert('%i18n:@some-error%'); (window as any).grecaptcha.reset(); this.recaptchaed = false; diff --git a/src/client/app/common/views/components/time.vue b/src/client/app/common/views/components/time.vue index 533958697c..6e0d2b0dcb 100644 --- a/src/client/app/common/views/components/time.vue +++ b/src/client/app/common/views/components/time.vue @@ -44,16 +44,16 @@ export default Vue.extend({ const time = this._time; const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/; return ( - ago >= 31536000 ? '%i18n:!common.time.years_ago%' .replace('{}', (~~(ago / 31536000)).toString()) : - ago >= 2592000 ? '%i18n:!common.time.months_ago%' .replace('{}', (~~(ago / 2592000)).toString()) : - ago >= 604800 ? '%i18n:!common.time.weeks_ago%' .replace('{}', (~~(ago / 604800)).toString()) : - ago >= 86400 ? '%i18n:!common.time.days_ago%' .replace('{}', (~~(ago / 86400)).toString()) : - ago >= 3600 ? '%i18n:!common.time.hours_ago%' .replace('{}', (~~(ago / 3600)).toString()) : - ago >= 60 ? '%i18n:!common.time.minutes_ago%'.replace('{}', (~~(ago / 60)).toString()) : - ago >= 10 ? '%i18n:!common.time.seconds_ago%'.replace('{}', (~~(ago % 60)).toString()) : - ago >= 0 ? '%i18n:!common.time.just_now%' : - ago < 0 ? '%i18n:!common.time.future%' : - '%i18n:!common.time.unknown%'); + ago >= 31536000 ? '%i18n:common.time.years_ago%' .replace('{}', (~~(ago / 31536000)).toString()) : + ago >= 2592000 ? '%i18n:common.time.months_ago%' .replace('{}', (~~(ago / 2592000)).toString()) : + ago >= 604800 ? '%i18n:common.time.weeks_ago%' .replace('{}', (~~(ago / 604800)).toString()) : + ago >= 86400 ? '%i18n:common.time.days_ago%' .replace('{}', (~~(ago / 86400)).toString()) : + ago >= 3600 ? '%i18n:common.time.hours_ago%' .replace('{}', (~~(ago / 3600)).toString()) : + ago >= 60 ? '%i18n:common.time.minutes_ago%'.replace('{}', (~~(ago / 60)).toString()) : + ago >= 10 ? '%i18n:common.time.seconds_ago%'.replace('{}', (~~(ago % 60)).toString()) : + ago >= 0 ? '%i18n:common.time.just_now%' : + ago < 0 ? '%i18n:common.time.future%' : + '%i18n:common.time.unknown%'); } }, created() { diff --git a/src/client/app/common/views/components/twitter-setting.vue b/src/client/app/common/views/components/twitter-setting.vue index ab07e6d09a..9a2a1c3d40 100644 --- a/src/client/app/common/views/components/twitter-setting.vue +++ b/src/client/app/common/views/components/twitter-setting.vue @@ -3,7 +3,7 @@ <p>%i18n:@description%<a :href="`${docsUrl}/link-to-twitter`" target="_blank">%i18n:@detail%</a></p> <p class="account" v-if="os.i.twitter" :title="`Twitter ID: ${os.i.twitter.userId}`">%i18n:@connected-to%: <a :href="`https://twitter.com/${os.i.twitter.screenName}`" target="_blank">@{{ os.i.twitter.screenName }}</a></p> <p> - <a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.twitter ? '%i18n:!@reconnect%' : '%i18n:!@connect%' }}</a> + <a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.twitter ? '%i18n:@reconnect%' : '%i18n:@connect%' }}</a> <span v-if="os.i.twitter"> or </span> <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.twitter" @click.prevent="disconnect">%i18n:@disconnect%</a> </p> diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue index 50f0877ae9..592367cd6d 100644 --- a/src/client/app/common/views/components/visibility-chooser.vue +++ b/src/client/app/common/views/components/visibility-chooser.vue @@ -5,34 +5,34 @@ <div @click="choose('public')" :class="{ active: v == 'public' }"> <div>%fa:globe%</div> <div> - <span>公開</span> + <span>%i18n:@public%</span> </div> </div> <div @click="choose('home')" :class="{ active: v == 'home' }"> <div>%fa:home%</div> <div> - <span>ホーム</span> - <span>ホームタイムラインにのみ公開</span> + <span>%i18n:@home%</span> + <span>%i18n:@home-desc%</span> </div> </div> <div @click="choose('followers')" :class="{ active: v == 'followers' }"> <div>%fa:unlock%</div> <div> - <span>フォロワー</span> - <span>自分のフォロワーにのみ公開</span> + <span>%i18n:@followers%</span> + <span>%i18n:@followers-desc%</span> </div> </div> <div @click="choose('specified')" :class="{ active: v == 'specified' }"> <div>%fa:envelope%</div> <div> - <span>ダイレクト</span> - <span>指定したユーザーにのみ公開</span> + <span>%i18n:@specified%</span> + <span>%i18n:@specified-desc%</span> </div> </div> <div @click="choose('private')" :class="{ active: v == 'private' }"> <div>%fa:lock%</div> <div> - <span>非公開</span> + <span>%i18n:@private%</span> </div> </div> </div> diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue index 6fadb030c3..cad59d24f0 100644 --- a/src/client/app/common/views/components/welcome-timeline.vue +++ b/src/client/app/common/views/components/welcome-timeline.vue @@ -37,6 +37,7 @@ export default Vue.extend({ fetch(cb?) { this.fetching = true; (this as any).api('notes', { + local: true, reply: false, renote: false, media: false, @@ -52,15 +53,15 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-welcome-timeline - background #fff +root(isDark) + background isDark ? #282C37 : #fff > div padding 16px overflow-wrap break-word font-size .9em - color #4C4C4C - border-bottom 1px solid rgba(#000, 0.05) + color isDark ? #fff : #4C4C4C + border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05) &:after content "" @@ -95,17 +96,23 @@ export default Vue.extend({ overflow hidden font-weight bold text-overflow ellipsis - color #627079 + color isDark ? #fff : #627079 > .username margin 0 .5em 0 0 - color #ccc + color isDark ? #606984 : #ccc > .info margin-left auto font-size 0.9em > .created-at - color #c0c0c0 + color isDark ? #606984 : #c0c0c0 + +.mk-welcome-timeline[data-darkmode] + root(true) + +.mk-welcome-timeline:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue index 75b1d60524..f337cec853 100644 --- a/src/client/app/common/views/widgets/broadcast.vue +++ b/src/client/app/common/views/widgets/broadcast.vue @@ -14,7 +14,7 @@ </svg> </div> <p class="fetching" v-if="fetching">%i18n:@fetching%<mk-ellipsis/></p> - <h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:!@no-broadcasts%' : broadcasts[i].title }}</h1> + <h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:@no-broadcasts%' : broadcasts[i].title }}</h1> <p v-if="!fetching"> <span v-if="broadcasts.length != 0" v-html="broadcasts[i].text"></span> <template v-if="broadcasts.length == 0">%i18n:@have-a-nice-day%</template> diff --git a/src/client/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue index e35462611d..75f5db808a 100644 --- a/src/client/app/common/views/widgets/donation.vue +++ b/src/client/app/common/views/widgets/donation.vue @@ -3,9 +3,9 @@ <article> <h1>%fa:heart%%i18n:@title%</h1> <p> - {{ '%i18n:!@text%'.substr(0, '%i18n:!@text%'.indexOf('{')) }} + {{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }} <a href="https://syuilo.com">@syuilo</a> - {{ '%i18n:!@text%'.substr('%i18n:!@text%'.indexOf('}') + 1) }} + {{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }} </p> </article> </div> diff --git a/src/client/app/config.ts b/src/client/app/config.ts index 522d7ff056..70c085de1c 100644 --- a/src/client/app/config.ts +++ b/src/client/app/config.ts @@ -8,6 +8,7 @@ declare const _STATS_URL_: string; declare const _STATUS_URL_: string; declare const _DEV_URL_: string; declare const _LANG_: string; +declare const _LANGS_: string; declare const _RECAPTCHA_SITEKEY_: string; declare const _SW_PUBLICKEY_: string; declare const _THEME_COLOR_: string; @@ -27,6 +28,7 @@ export const statsUrl = _STATS_URL_; export const statusUrl = _STATUS_URL_; export const devUrl = _DEV_URL_; export const lang = _LANG_; +export const langs = _LANGS_; export const recaptchaSitekey = _RECAPTCHA_SITEKEY_; export const swPublickey = _SW_PUBLICKEY_; export const themeColor = _THEME_COLOR_; diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue index 757eefac7e..9a93841e52 100644 --- a/src/client/app/desktop/views/components/calendar.vue +++ b/src/client/app/desktop/views/components/calendar.vue @@ -2,7 +2,7 @@ <div class="mk-calendar" :data-melt="design == 4 || design == 5"> <template v-if="design == 0 || design == 1"> <button @click="prev" title="%i18n:@prev%">%fa:chevron-circle-left%</button> - <p class="title">{{ '%i18n:!@title%'.replace('{1}', year).replace('{2}', month) }}</p> + <p class="title">{{ '%i18n:@title%'.replace('{1}', year).replace('{2}', month) }}</p> <button @click="next" title="%i18n:@next%">%fa:chevron-circle-right%</button> </template> @@ -21,7 +21,7 @@ :data-is-out-of-range="isOutOfRange(i + 1)" :data-is-donichi="isDonichi(i + 1)" @click="go(i + 1)" - :title="isOutOfRange(i + 1) ? null : '%i18n:!@go%'" + :title="isOutOfRange(i + 1) ? null : '%i18n:@go%'" > <div>{{ i + 1 }}</div> </div> @@ -58,13 +58,13 @@ export default Vue.extend({ month: new Date().getMonth() + 1, selected: new Date(), weekdayText: [ - '%i18n:!common.weekday-short.sunday%', - '%i18n:!common.weekday-short.monday%', - '%i18n:!common.weekday-short.tuesday%', - '%i18n:!common.weekday-short.wednesday%', - '%i18n:!common.weekday-short.thursday%', - '%i18n:!common.weekday-short.friday%', - '%i18n:!common.weekday-short.saturday%' + '%i18n:common.weekday-short.sunday%', + '%i18n:common.weekday-short.monday%', + '%i18n:common.weekday-short.tuesday%', + '%i18n:common.weekday-short.wednesday%', + '%i18n:common.weekday-short.thursday%', + '%i18n:common.weekday-short.friday%', + '%i18n:common.weekday-short.saturday%' ] }; }, diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue index d8b8420ece..fb553e1ae7 100644 --- a/src/client/app/desktop/views/components/drive.file.vue +++ b/src/client/app/desktop/views/components/drive.file.vue @@ -64,46 +64,46 @@ export default Vue.extend({ this.isContextmenuShowing = true; contextmenu(e, [{ type: 'item', - text: '%i18n:!@contextmenu.rename%', + text: '%i18n:@contextmenu.rename%', icon: '%fa:i-cursor%', onClick: this.rename }, { type: 'item', - text: '%i18n:!@contextmenu.copy-url%', + text: '%i18n:@contextmenu.copy-url%', icon: '%fa:link%', onClick: this.copyUrl }, { type: 'link', href: `${this.file.url}?download`, - text: '%i18n:!@contextmenu.download%', + text: '%i18n:@contextmenu.download%', icon: '%fa:download%', }, { type: 'divider', }, { type: 'item', - text: '%i18n:!common.delete%', + text: '%i18n:common.delete%', icon: '%fa:R trash-alt%', onClick: this.deleteFile }, { type: 'divider', }, { type: 'nest', - text: '%i18n:!@contextmenu.else-files%', + text: '%i18n:@contextmenu.else-files%', menu: [{ type: 'item', - text: '%i18n:!@contextmenu.set-as-avatar%', + text: '%i18n:@contextmenu.set-as-avatar%', onClick: this.setAsAvatar }, { type: 'item', - text: '%i18n:!@contextmenu.set-as-banner%', + text: '%i18n:@contextmenu.set-as-banner%', onClick: this.setAsBanner }] }, { type: 'nest', - text: '%i18n:!@contextmenu.open-in-app%', + text: '%i18n:@contextmenu.open-in-app%', menu: [{ type: 'item', - text: '%i18n:!@contextmenu.add-app%...', + text: '%i18n:@contextmenu.add-app%...', onClick: this.addApp }] }], { @@ -141,8 +141,8 @@ export default Vue.extend({ rename() { (this as any).apis.input({ - title: '%i18n:!@contextmenu.rename-file%', - placeholder: '%i18n:!@contextmenu.input-new-file-name%', + title: '%i18n:@contextmenu.rename-file%', + placeholder: '%i18n:@contextmenu.input-new-file-name%', default: this.file.name, allowEmpty: false }).then(name => { @@ -157,9 +157,9 @@ export default Vue.extend({ copyToClipboard(this.file.url); (this as any).apis.dialog({ title: '%fa:check%%i18n:@contextmenu.copied%', - text: '%i18n:!@contextmenu.copied-url-to-clipboard%', + text: '%i18n:@contextmenu.copied-url-to-clipboard%', actions: [{ - text: '%i18n:!common.ok%' + text: '%i18n:common.ok%' }] }); }, diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue index 0761ffb1a1..16f474f4e0 100644 --- a/src/client/app/desktop/views/components/drive.folder.vue +++ b/src/client/app/desktop/views/components/drive.folder.vue @@ -54,26 +54,26 @@ export default Vue.extend({ this.isContextmenuShowing = true; contextmenu(e, [{ type: 'item', - text: '%i18n:!@contextmenu.move-to-this-folder%', + text: '%i18n:@contextmenu.move-to-this-folder%', icon: '%fa:arrow-right%', onClick: this.go }, { type: 'item', - text: '%i18n:!@contextmenu.show-in-new-window%', + text: '%i18n:@contextmenu.show-in-new-window%', icon: '%fa:R window-restore%', onClick: this.newWindow }, { type: 'divider', }, { type: 'item', - text: '%i18n:!@contextmenu.rename%', + text: '%i18n:@contextmenu.rename%', icon: '%fa:i-cursor%', onClick: this.rename }, { type: 'divider', }, { type: 'item', - text: '%i18n:!common.delete%', + text: '%i18n:common.delete%', icon: '%fa:R trash-alt%', onClick: this.deleteFolder }], { @@ -159,15 +159,15 @@ export default Vue.extend({ switch (err) { case 'detected-circular-definition': (this as any).apis.dialog({ - title: '%fa:exclamation-triangle%%i18n:!@unable-to-process%', - text: '%i18n:!@circular-reference-detected%', + title: '%fa:exclamation-triangle%%i18n:@unable-to-process%', + text: '%i18n:@circular-reference-detected%', actions: [{ - text: '%i18n:!common.ok%' + text: '%i18n:common.ok%' }] }); break; default: - alert('%i18n:!@unhandled-error% ' + err); + alert('%i18n:@unhandled-error% ' + err); } }); } @@ -199,8 +199,8 @@ export default Vue.extend({ rename() { (this as any).apis.input({ - title: '%i18n:!@contextmenu.rename-folder%', - placeholder: '%i18n:!@contextmenu.input-new-folder-name%', + title: '%i18n:@contextmenu.rename-folder%', + placeholder: '%i18n:@contextmenu.input-new-folder-name%', default: this.folder.name }).then(name => { (this as any).api('drive/folders/update', { diff --git a/src/client/app/desktop/views/components/drive.nav-folder.vue b/src/client/app/desktop/views/components/drive.nav-folder.vue index 71b2e419d9..40f620875e 100644 --- a/src/client/app/desktop/views/components/drive.nav-folder.vue +++ b/src/client/app/desktop/views/components/drive.nav-folder.vue @@ -8,7 +8,7 @@ @drop.stop="onDrop" > <template v-if="folder == null">%fa:cloud%</template> - <span>{{ folder == null ? '%i18n:!@drive%' : folder.name }}</span> + <span>{{ folder == null ? '%i18n:@drive%' : folder.name }}</span> </div> </template> diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue index 973df1014d..cae40f306c 100644 --- a/src/client/app/desktop/views/components/drive.vue +++ b/src/client/app/desktop/views/components/drive.vue @@ -138,17 +138,17 @@ export default Vue.extend({ onContextmenu(e) { contextmenu(e, [{ type: 'item', - text: '%i18n:!@contextmenu.create-folder%', + text: '%i18n:@contextmenu.create-folder%', icon: '%fa:R folder%', onClick: this.createFolder }, { type: 'item', - text: '%i18n:!@contextmenu.upload%', + text: '%i18n:@contextmenu.upload%', icon: '%fa:upload%', onClick: this.selectLocalFile }, { type: 'item', - text: '%i18n:!@contextmenu.url-upload%', + text: '%i18n:@contextmenu.url-upload%', icon: '%fa:cloud-upload-alt%', onClick: this.urlUpload }]); @@ -306,15 +306,15 @@ export default Vue.extend({ switch (err) { case 'detected-circular-definition': (this as any).apis.dialog({ - title: '%fa:exclamation-triangle%%i18n:!@unable-to-process%', - text: '%i18n:!@circular-reference-detected%', + title: '%fa:exclamation-triangle%%i18n:@unable-to-process%', + text: '%i18n:@circular-reference-detected%', actions: [{ - text: '%i18n:!common.ok%' + text: '%i18n:common.ok%' }] }); break; default: - alert('%i18n:!@unhandled-error% ' + err); + alert('%i18n:@unhandled-error% ' + err); } }); } @@ -327,8 +327,8 @@ export default Vue.extend({ urlUpload() { (this as any).apis.input({ - title: '%i18n:!@url-upload%', - placeholder: '%i18n:!@url-of-file%' + title: '%i18n:@url-upload%', + placeholder: '%i18n:@url-of-file%' }).then(url => { (this as any).api('drive/files/upload_from_url', { url: url, @@ -337,9 +337,9 @@ export default Vue.extend({ (this as any).apis.dialog({ title: '%fa:check%%i18n:@url-upload-requested%', - text: '%i18n:!@may-take-time%', + text: '%i18n:@may-take-time%', actions: [{ - text: '%i18n:!common.ok%' + text: '%i18n:common.ok%' }] }); }); @@ -347,8 +347,8 @@ export default Vue.extend({ createFolder() { (this as any).apis.input({ - title: '%i18n:!@create-folder%', - placeholder: '%i18n:!@folder-name%' + title: '%i18n:@create-folder%', + placeholder: '%i18n:@folder-name%' }).then(name => { (this as any).api('drive/folders/create', { name: name, diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue index f3eec13e0b..7ed31315f1 100644 --- a/src/client/app/desktop/views/components/followers-window.vue +++ b/src/client/app/desktop/views/components/followers-window.vue @@ -1,7 +1,7 @@ <template> <mk-window width="400px" height="550px" @closed="$destroy"> <span slot="header" :class="$style.header"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>%i18n:!@followers%.replace('{}', {{ user | userName }}) + <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }} </span> <mk-followers :user="user"/> </mk-window> @@ -11,7 +11,12 @@ import Vue from 'vue'; export default Vue.extend({ - props: ['user'] + props: ['user'], + computed: { + name(): string { + return Vue.filter('userName')(this.user); + } + } }); </script> diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue index 153819b12e..b97f21e2a3 100644 --- a/src/client/app/desktop/views/components/following-window.vue +++ b/src/client/app/desktop/views/components/following-window.vue @@ -1,7 +1,7 @@ <template> <mk-window width="400px" height="550px" @closed="$destroy"> <span slot="header" :class="$style.header"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>%i18n:!@following%.replace('{}', {{ user | userName }}) + <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }} </span> <mk-following :user="user"/> </mk-window> @@ -11,7 +11,12 @@ import Vue from 'vue'; export default Vue.extend({ - props: ['user'] + props: ['user'], + computed: { + name(): string { + return Vue.filter('userName')(this.user); + } + } }); </script> diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue index 87dae5a806..d84c1e404f 100644 --- a/src/client/app/desktop/views/components/home.vue +++ b/src/client/app/desktop/views/components/home.vue @@ -102,7 +102,7 @@ export default Vue.extend({ computed: { home(): any[] { - return this.$store.state.settings.data.home; + return this.$store.state.settings.home; }, left(): any[] { return this.home.filter(w => w.place == 'left'); diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue index bda53db918..5b48b7a1ba 100644 --- a/src/client/app/desktop/views/components/note-detail.vue +++ b/src/client/app/desktop/views/components/note-detail.vue @@ -2,16 +2,16 @@ <div class="mk-note-detail" :title="title"> <button class="read-more" - v-if="p.reply && p.reply.replyId && context.length == 0" - title="会話をもっと読み込む" - @click="fetchContext" - :disabled="contextFetching" + v-if="p.reply && p.reply.replyId && conversation.length == 0" + title="%i18n:@more%" + @click="fetchConversation" + :disabled="conversationFetching" > - <template v-if="!contextFetching">%fa:ellipsis-v%</template> - <template v-if="contextFetching">%fa:spinner .pulse%</template> + <template v-if="!conversationFetching">%fa:ellipsis-v%</template> + <template v-if="conversationFetching">%fa:spinner .pulse%</template> </button> - <div class="context"> - <x-sub v-for="note in context" :key="note.id" :note="note"/> + <div class="conversation"> + <x-sub v-for="note in conversation" :key="note.id" :note="note"/> </div> <div class="reply-to" v-if="p.reply"> <x-sub :note="p.reply"/> @@ -21,7 +21,10 @@ <mk-avatar class="avatar" :user="note.user"/> %fa:retweet% <router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link> - がRenote + <span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span> + <a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a> + <span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span> + <mk-time :time="note.createdAt"/> </p> </div> <article> @@ -35,7 +38,7 @@ </header> <div class="body"> <div class="text"> - <span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span> + <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span> <mk-note-html v-if="p.text" :text="p.text" :i="os.i"/> </div> <div class="media" v-if="p.media.length > 0"> @@ -46,7 +49,7 @@ <div class="tags" v-if="p.tags && p.tags.length > 0"> <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> </div> - <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> + <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <div class="map" v-if="p.geo" ref="map"></div> <div class="renote" v-if="p.renote"> <mk-note-preview :note="p.renote"/> @@ -54,15 +57,15 @@ </div> <footer> <mk-reactions-viewer :note="p"/> - <button @click="reply" title="返信"> + <button @click="reply" title=""> <template v-if="p.reply">%fa:reply-all%</template> <template v-else>%fa:reply%</template> <p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> </button> - <button @click="renote" title="Renote"> + <button @click="renote" title="%i18n:@renote%"> %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> </button> - <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="リアクション"> + <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@add-reaction%"> %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> </button> <button @click="menu" ref="menuButton"> @@ -104,8 +107,8 @@ export default Vue.extend({ data() { return { - context: [], - contextFetching: false, + conversation: [], + conversationFetching: false, replies: [] }; }, @@ -173,15 +176,15 @@ export default Vue.extend({ }, methods: { - fetchContext() { - this.contextFetching = true; + fetchConversation() { + this.conversationFetching = true; - // Fetch context - (this as any).api('notes/context', { + // Fetch conversation + (this as any).api('notes/conversation', { noteId: this.p.replyId - }).then(context => { - this.contextFetching = false; - this.context = context.reverse(); + }).then(conversation => { + this.conversationFetching = false; + this.conversation = conversation.reverse(); }); }, reply() { @@ -246,7 +249,7 @@ root(isDark) &:disabled color isDark ? #21242b : #ccc - > .context + > .conversation > * border-bottom 1px solid isDark ? #1c2023 : #eef0f2 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 503982b1a8..5f0c46b4c6 100644 --- a/src/client/app/desktop/views/components/notes.note.sub.vue +++ b/src/client/app/desktop/views/components/notes.note.sub.vue @@ -4,6 +4,9 @@ <div class="main"> <header> <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> + <span class="is-admin" v-if="note.user.isAdmin">admin</span> + <span class="is-bot" v-if="note.user.isBot">bot</span> + <span class="is-cat" v-if="note.user.isCat">cat</span> <span class="username"><mk-acct :user="note.user"/></span> <div class="info"> <span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span> @@ -68,7 +71,6 @@ root(isDark) align-items baseline margin-bottom 2px white-space nowrap - line-height 21px > .name display block @@ -84,6 +86,20 @@ root(isDark) &:hover text-decoration underline + > .is-admin + > .is-bot + > .is-cat + margin 0 0.5em 0 0 + padding 1px 5px + font-size 10px + color isDark ? #758188 : #aaa + border solid 1px isDark ? #57616f : #ddd + border-radius 3px + + &.is-admin + border-color isDark ? #d42c41 : #f56a7b + color isDark ? #d42c41 : #f56a7b + > .username margin 0 .5em 0 0 color isDark ? #606984 : #d1d8da diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue index e23d3e5a52..4448820eb9 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -6,9 +6,9 @@ <div class="renote" v-if="isRenote"> <mk-avatar class="avatar" :user="note.user"/> %fa:retweet% - <span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span> + <span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span> <a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a> - <span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span> + <span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span> <mk-time :time="note.createdAt"/> </div> <article> @@ -16,7 +16,9 @@ <div class="main"> <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="is-admin" v-if="p.user.isAdmin">admin</span> + <span class="is-bot" v-if="p.user.isBot">bot</span> + <span class="is-cat" v-if="p.user.isCat">cat</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> @@ -430,7 +432,9 @@ root(isDark) &:hover text-decoration underline + > .is-admin > .is-bot + > .is-cat margin 0 .5em 0 0 padding 1px 6px font-size 12px @@ -438,6 +442,10 @@ root(isDark) border solid 1px isDark ? #57616f : #ddd border-radius 3px + &.is-admin + border-color isDark ? #d42c41 : #f56a7b + color isDark ? #d42c41 : #f56a7b + > .username margin 0 .5em 0 0 overflow hidden diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index c041e5278c..55b0de3fbd 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -145,9 +145,9 @@ export default Vue.extend({ this.notes.unshift(note); // サウンドを再生する - if ((this as any).os.isEnableSounds && !silent) { + if (this.$store.state.device.enableSounds && !silent) { const sound = new Audio(`${url}/assets/post.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; + sound.volume = this.$store.state.device.soundVolume; sound.play(); } diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue index 7923d1a62d..5564dad623 100644 --- a/src/client/app/desktop/views/components/notifications.vue +++ b/src/client/app/desktop/views/components/notifications.vue @@ -81,7 +81,7 @@ </transition-group> </div> <button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> - <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:!common.loading%' : '%i18n:!@more%' }} + <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }} </button> <p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p> <p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue index 1f0fbff760..18bb39f9bc 100644 --- a/src/client/app/desktop/views/components/post-form-window.vue +++ b/src/client/app/desktop/views/components/post-form-window.vue @@ -4,8 +4,8 @@ <span :class="$style.icon" v-if="geo">%fa:map-marker-alt%</span> <span v-if="!reply">%i18n:@note%</span> <span v-if="reply">%i18n:@reply%</span> - <span :class="$style.count" v-if="media.length != 0">{{ '%i18n:!@attaches%'.replace('{}', media.length) }}</span> - <span :class="$style.count" v-if="uploadings.length != 0">{{ '%i18n:!@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span> + <span :class="$style.count" v-if="media.length != 0">{{ '%i18n:@attaches%'.replace('{}', media.length) }}</span> + <span :class="$style.count" v-if="uploadings.length != 0">{{ '%i18n:@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span> </span> <mk-note-preview v-if="reply" :class="$style.notePreview" :note="reply"/> diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue index 984fc9866c..0696d4e82b 100644 --- a/src/client/app/desktop/views/components/post-form.vue +++ b/src/client/app/desktop/views/components/post-form.vue @@ -37,7 +37,7 @@ <button class="visibility" title="公開範囲" @click="setVisibility" ref="visibilityButton">%fa:lock%</button> <p class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</p> <button :class="{ posting }" class="submit" :disabled="!canPost" @click="post"> - {{ posting ? '%i18n:!@posting%' : submitText }}<mk-ellipsis v-if="posting"/> + {{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/> </button> <input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" @change="onChangeFile"/> <div class="dropzone" v-if="draghover"></div> @@ -86,18 +86,18 @@ export default Vue.extend({ placeholder(): string { return this.renote - ? '%i18n:!@quote-placeholder%' + ? '%i18n:@quote-placeholder%' : this.reply - ? '%i18n:!@reply-placeholder%' - : '%i18n:!@note-placeholder%'; + ? '%i18n:@reply-placeholder%' + : '%i18n:@note-placeholder%'; }, submitText(): string { return this.renote - ? '%i18n:!@renote%' + ? '%i18n:@renote%' : this.reply - ? '%i18n:!@reply%' - : '%i18n:!@note%'; + ? '%i18n:@reply%' + : '%i18n:@note%'; }, canPost(): boolean { @@ -304,16 +304,16 @@ export default Vue.extend({ this.deleteDraft(); this.$emit('posted'); (this as any).apis.notify(this.renote - ? '%i18n:!@reposted%' + ? '%i18n:@reposted%' : this.reply - ? '%i18n:!@replied%' - : '%i18n:!@posted%'); + ? '%i18n:@replied%' + : '%i18n:@posted%'); }).catch(err => { (this as any).apis.notify(this.renote - ? '%i18n:!@renote-failed%' + ? '%i18n:@renote-failed%' : this.reply - ? '%i18n:!@reply-failed%' - : '%i18n:!@note-failed%'); + ? '%i18n:@reply-failed%' + : '%i18n:@note-failed%'); }).then(() => { this.posting = false; }); diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue index 9c0154211b..38eab3362f 100644 --- a/src/client/app/desktop/views/components/renote-form.vue +++ b/src/client/app/desktop/views/components/renote-form.vue @@ -5,7 +5,7 @@ <footer> <a class="quote" v-if="!quote" @click="onQuote">%i18n:@quote%</a> <button class="ui cancel" @click="cancel">%i18n:@cancel%</button> - <button class="ui primary ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:!@reposting%' : '%i18n:!@renote%' }}</button> + <button class="ui primary ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:@reposting%' : '%i18n:@renote%' }}</button> </footer> </template> <template v-if="quote"> @@ -32,9 +32,9 @@ export default Vue.extend({ renoteId: this.note.id }).then(data => { this.$emit('posted'); - (this as any).apis.notify('%i18n:!@success%'); + (this as any).apis.notify('%i18n:@success%'); }).catch(err => { - (this as any).apis.notify('%i18n:!@failure%'); + (this as any).apis.notify('%i18n:@failure%'); }).then(() => { this.wait = false; }); diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue index d5be177dcc..deb865b102 100644 --- a/src/client/app/desktop/views/components/settings-window.vue +++ b/src/client/app/desktop/views/components/settings-window.vue @@ -1,6 +1,6 @@ <template> <mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy"> - <span slot="header" :class="$style.header">%fa:cog%設定</span> + <span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span> <mk-settings @done="close"/> </mk-window> </template> diff --git a/src/client/app/desktop/views/components/settings.2fa.vue b/src/client/app/desktop/views/components/settings.2fa.vue index 99b6cb947c..0809dd798c 100644 --- a/src/client/app/desktop/views/components/settings.2fa.vue +++ b/src/client/app/desktop/views/components/settings.2fa.vue @@ -34,7 +34,7 @@ export default Vue.extend({ methods: { register() { (this as any).apis.input({ - title: '%i18n:!@enter-password%', + title: '%i18n:@enter-password%', type: 'password' }).then(password => { (this as any).api('i/2fa/register', { @@ -47,13 +47,13 @@ export default Vue.extend({ unregister() { (this as any).apis.input({ - title: '%i18n:!@enter-password%', + title: '%i18n:@enter-password%', type: 'password' }).then(password => { (this as any).api('i/2fa/unregister', { password: password }).then(() => { - (this as any).apis.notify('%i18n:!@unregistered%'); + (this as any).apis.notify('%i18n:@unregistered%'); (this as any).os.i.twoFactorEnabled = false; }); }); @@ -63,10 +63,10 @@ export default Vue.extend({ (this as any).api('i/2fa/done', { token: this.token }).then(() => { - (this as any).apis.notify('%i18n:!@success%'); + (this as any).apis.notify('%i18n:@success%'); (this as any).os.i.twoFactorEnabled = true; }).catch(() => { - (this as any).apis.notify('%i18n:!@failed%'); + (this as any).apis.notify('%i18n:@failed%'); }); } } diff --git a/src/client/app/desktop/views/components/settings.api.vue b/src/client/app/desktop/views/components/settings.api.vue index b22ee6cdab..b8eef3de63 100644 --- a/src/client/app/desktop/views/components/settings.api.vue +++ b/src/client/app/desktop/views/components/settings.api.vue @@ -15,7 +15,7 @@ export default Vue.extend({ methods: { regenerateToken() { (this as any).apis.input({ - title: '%i18n:!@enter-password%', + title: '%i18n:@enter-password%', type: 'password' }).then(password => { (this as any).api('i/regenerate_token', { diff --git a/src/client/app/desktop/views/components/settings.password.vue b/src/client/app/desktop/views/components/settings.password.vue index 9e89bc0f6e..39896daf67 100644 --- a/src/client/app/desktop/views/components/settings.password.vue +++ b/src/client/app/desktop/views/components/settings.password.vue @@ -11,21 +11,21 @@ export default Vue.extend({ methods: { reset() { (this as any).apis.input({ - title: '%i18n:!@enter-current-password%', + title: '%i18n:@enter-current-password%', type: 'password' }).then(currentPassword => { (this as any).apis.input({ - title: '%i18n:!@enter-new-password%', + title: '%i18n:@enter-new-password%', type: 'password' }).then(newPassword => { (this as any).apis.input({ - title: '%i18n:!@enter-new-password-again%', + title: '%i18n:@enter-new-password-again%', type: 'password' }).then(newPassword2 => { if (newPassword !== newPassword2) { (this as any).apis.dialog({ title: null, - text: '%i18n:!@not-match%', + text: '%i18n:@not-match%', actions: [{ text: 'OK' }] @@ -36,7 +36,7 @@ export default Vue.extend({ currentPasword: currentPassword, newPassword: newPassword }).then(() => { - (this as any).apis.notify('%i18n:!@changed%'); + (this as any).apis.notify('%i18n:@changed%'); }); }); }); diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue index 84b09eb988..132ab12f1c 100644 --- a/src/client/app/desktop/views/components/settings.profile.vue +++ b/src/client/app/desktop/views/components/settings.profile.vue @@ -24,7 +24,8 @@ <button class="ui primary" @click="save">%i18n:@save%</button> <section> <h2>その他</h2> - <mk-switch v-model="os.i.isBot" @change="onChangeIsBot" text="このアカウントはbotです"/> + <mk-switch v-model="os.i.isBot" @change="onChangeIsBot" text="%i18n:@is-bot%"/> + <mk-switch v-model="os.i.isCat" @change="onChangeIsCat" text="%i18n:@is-cat%"/> </section> </div> </template> @@ -65,6 +66,11 @@ export default Vue.extend({ (this as any).api('i/update', { isBot: (this as any).os.i.isBot }); + }, + onChangeIsCat() { + (this as any).api('i/update', { + isCat: (this as any).os.i.isCat + }); } } }); diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index 6652a8ac37..dac5fe67cb 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -62,8 +62,10 @@ <el-slider v-model="soundVolume" :show-input="true" - :format-tooltip="v => `${v}%`" + :format-tooltip="v => `${v * 100}%`" :disabled="!enableSounds" + :max="1" + :step="0.1" /> <button class="ui button" @click="soundTest">%fa:volume-up% %i18n:@test%</button> </section> @@ -77,14 +79,10 @@ <h1>%i18n:@language%</h1> <el-select v-model="lang" placeholder="%i18n:@pick-language%"> <el-option-group label="%i18n:@recommended%"> - <el-option label="%i18n:@auto%" value=""/> + <el-option label="%i18n:@auto%" :value="null"/> </el-option-group> <el-option-group label="%i18n:@specify-language%"> - <el-option label="日本語" value="ja"/> - <el-option label="English" value="en"/> - <el-option label="Français" value="fr"/> - <el-option label="Polski" value="pl"/> - <el-option label="Deutsch" value="de"/> + <el-option v-for="x in langs" :label="x[1]" :value="x[0]" :key="x[0]"/> </el-option-group> </el-select> <div class="none ui info"> @@ -178,15 +176,7 @@ <mk-switch v-model="debug" text="%i18n:@debug-mode%"> <span>%i18n:@debug-mode-desc%</span> </mk-switch> - <template v-if="debug"> - <mk-switch v-model="useRawScript" text="%i18n:@use-raw-script%"> - <span>%i18n:@use-raw-script-desc%</span> - </mk-switch> - <div class="none ui info"> - <p>%fa:info-circle%%i18n:@source-info%</p> - </div> - </template> - <mk-switch v-model="enableExperimental" text="%i18n:@experimental%"> + <mk-switch v-model="enableExperimentalFeatures" text="%i18n:@experimental%"> <span>%i18n:@experimental-desc%</span> </mk-switch> <details v-if="debug"> @@ -214,7 +204,7 @@ import XApi from './settings.api.vue'; import XApps from './settings.apps.vue'; import XSignins from './settings.signins.vue'; import XDrive from './settings.drive.vue'; -import { url, docsUrl, license, lang, version } from '../../../config'; +import { url, docsUrl, license, lang, langs, version } from '../../../config'; import checkForUpdate from '../../../common/scripts/check-for-update'; import MkTaskManager from './taskmanager.vue'; @@ -235,55 +225,59 @@ export default Vue.extend({ meta: null, license, version, + langs, latestVersion: undefined, - checkingForUpdate: false, - darkmode: localStorage.getItem('darkmode') == 'true', - enableSounds: localStorage.getItem('enableSounds') == 'true', - autoPopout: localStorage.getItem('autoPopout') == 'true', - apiViaStream: localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true, - soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 50, - lang: localStorage.getItem('lang') || '', - preventUpdate: localStorage.getItem('preventUpdate') == 'true', - debug: localStorage.getItem('debug') == 'true', - useRawScript: localStorage.getItem('useRawScript') == 'true', - enableExperimental: localStorage.getItem('enableExperimental') == 'true' + checkingForUpdate: false }; }, computed: { licenseUrl(): string { return `${docsUrl}/${lang}/license`; - } - }, - watch: { - autoPopout() { - localStorage.setItem('autoPopout', this.autoPopout ? 'true' : 'false'); }, - apiViaStream() { - localStorage.setItem('apiViaStream', this.apiViaStream ? 'true' : 'false'); + + apiViaStream: { + get() { return this.$store.state.device.apiViaStream; }, + set(value) { this.$store.commit('device/set', { key: 'apiViaStream', value }); } }, - darkmode() { - (this as any)._updateDarkmode_(this.darkmode); + + autoPopout: { + get() { return this.$store.state.device.autoPopout; }, + set(value) { this.$store.commit('device/set', { key: 'autoPopout', value }); } }, - enableSounds() { - localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false'); + + darkmode: { + get() { return this.$store.state.device.darkmode; }, + set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); } }, - soundVolume() { - localStorage.setItem('soundVolume', this.soundVolume.toString()); + + enableSounds: { + get() { return this.$store.state.device.enableSounds; }, + set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); } }, - lang() { - localStorage.setItem('lang', this.lang); + + soundVolume: { + get() { return this.$store.state.device.soundVolume; }, + set(value) { this.$store.commit('device/set', { key: 'soundVolume', value }); } }, - preventUpdate() { - localStorage.setItem('preventUpdate', this.preventUpdate ? 'true' : 'false'); + + lang: { + get() { return this.$store.state.device.lang; }, + set(value) { this.$store.commit('device/set', { key: 'lang', value }); } }, - debug() { - localStorage.setItem('debug', this.debug ? 'true' : 'false'); + + preventUpdate: { + get() { return this.$store.state.device.preventUpdate; }, + set(value) { this.$store.commit('device/set', { key: 'preventUpdate', value }); } }, - useRawScript() { - localStorage.setItem('useRawScript', this.useRawScript ? 'true' : 'false'); + + debug: { + get() { return this.$store.state.device.debug; }, + set(value) { this.$store.commit('device/set', { key: 'debug', value }); } }, - enableExperimental() { - localStorage.setItem('enableExperimental', this.enableExperimental ? 'true' : 'false'); + + enableExperimentalFeatures: { + get() { return this.$store.state.device.enableExperimentalFeatures; }, + set(value) { this.$store.commit('device/set', { key: 'enableExperimentalFeatures', value }); } } }, created() { @@ -371,13 +365,13 @@ export default Vue.extend({ this.latestVersion = newer; if (newer == null) { (this as any).apis.dialog({ - title: '%i18n:!@no-updates%', - text: '%i18n:!@no-updates-desc%' + title: '%i18n:@no-updates%', + text: '%i18n:@no-updates-desc%' }); } else { (this as any).apis.dialog({ - title: '%i18n:!@update-available%', - text: '%i18n:!@update-available-desc%' + title: '%i18n:@update-available%', + text: '%i18n:@update-available-desc%' }); } }); @@ -385,13 +379,13 @@ export default Vue.extend({ clean() { localStorage.clear(); (this as any).apis.dialog({ - title: '%i18n:!@cache-cleared%', - text: '%i18n:!@caache-cleared-desc%' + title: '%i18n:@cache-cleared%', + text: '%i18n:@caache-cleared-desc%' }); }, soundTest() { const sound = new Audio(`${url}/assets/message.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; + sound.volume = this.$store.state.device.soundVolume; sound.play(); } } diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue index dd4012039b..03b634b0ff 100644 --- a/src/client/app/desktop/views/components/sub-note-content.vue +++ b/src/client/app/desktop/views/components/sub-note-content.vue @@ -1,17 +1,17 @@ <template> <div class="mk-sub-note-content"> <div class="body"> - <span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span> + <span v-if="note.isHidden" style="opacity: 0.5">%i18n:@hidden%</span> <a class="reply" v-if="note.replyId">%fa:reply%</a> <mk-note-html :text="note.text" :i="os.i"/> <a class="rp" v-if="note.renoteId" :href="`/note:${note.renoteId}`">RP: ...</a> </div> <details v-if="note.media.length > 0"> - <summary>({{ note.media.length }}つのメディア)</summary> + <summary>({{ note.media.length }}%i18n:@media%)</summary> <mk-media-list :media-list="note.media"/> </details> <details v-if="note.poll"> - <summary>投票</summary> + <summary>%i18n:@poll%</summary> <mk-poll :note="note"/> </details> </div> diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue index fd15ea6006..f3f6539496 100644 --- a/src/client/app/desktop/views/components/ui.header.account.vue +++ b/src/client/app/desktop/views/components/ui.header.account.vue @@ -35,7 +35,7 @@ </ul> <ul> <li @click="dark"> - <p><span>%i18n:@dark%</span><template v-if="_darkmode_">%fa:moon%</template><template v-else>%fa:R moon%</template></p> + <p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p> </li> </ul> </div> @@ -99,7 +99,10 @@ export default Vue.extend({ (this as any).os.signout(); }, dark() { - (this as any)._updateDarkmode_(!(this as any)._darkmode_); + this.$store.commit('device/set', { + key: 'darkmode', + value: !this.$store.state.device.darkmode + }); } } }); 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 585c0a864f..454c725d20 100644 --- a/src/client/app/desktop/views/components/user-lists-window.vue +++ b/src/client/app/desktop/views/components/user-lists-window.vue @@ -2,7 +2,7 @@ <mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy"> <span slot="header">%fa:list% リスト</span> - <div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="_darkmode_"> + <div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="$store.state.device.darkmode"> <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> diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue index dbad295178..262fd38cd1 100644 --- a/src/client/app/desktop/views/components/users-list.item.vue +++ b/src/client/app/desktop/views/components/users-list.item.vue @@ -7,7 +7,7 @@ <span class="username">@{{ user | acct }}</span> </header> <div class="body"> - <p class="followed" v-if="user.isFollowed">フォローされています</p> + <p class="followed" v-if="user.isFollowed">%i18n:@followed%</p> <div class="description">{{ user.description }}</div> </div> </div> diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue index 2e7eb557b4..ac06ac8e57 100644 --- a/src/client/app/desktop/views/components/window.vue +++ b/src/client/app/desktop/views/components/window.vue @@ -9,8 +9,8 @@ > <h1><slot name="header"></slot></h1> <div> - <button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="ポップアウト">%fa:R window-restore%</button> - <button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="閉じる">%fa:times%</button> + <button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="%i18n:@popout%">%fa:R window-restore%</button> + <button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="%i18n:@close%">%fa:times%</button> </div> </header> <div class="content"> @@ -95,7 +95,7 @@ export default Vue.extend({ }, created() { - if (localStorage.getItem('autoPopout') == 'true' && this.popoutUrl) { + if ((this as any).os.store.state.device.autoPopout && this.popoutUrl) { this.popout(); this.preventMount = true; } else { diff --git a/src/client/app/desktop/views/pages/drive.vue b/src/client/app/desktop/views/pages/drive.vue index 353f59b703..217dcb7751 100644 --- a/src/client/app/desktop/views/pages/drive.vue +++ b/src/client/app/desktop/views/pages/drive.vue @@ -16,11 +16,11 @@ export default Vue.extend({ this.folder = this.$route.params.folder; }, mounted() { - document.title = 'Misskey Drive'; + document.title = '%i18n:@title%'; }, methods: { onMoveRoot() { - const title = 'Misskey Drive'; + const title = '%i18n:@title%'; // Rewrite URL history.pushState(null, title, '/i/drive'); @@ -28,7 +28,7 @@ export default Vue.extend({ document.title = title; }, onOpenFolder(folder) { - const title = folder.name + ' | Misskey Drive'; + const title = folder.name + ' | %i18n:@title%'; // Rewrite URL history.pushState(null, title, '/i/drive/folder/' + folder.id); @@ -49,4 +49,3 @@ export default Vue.extend({ > .mk-drive height 100% </style> - diff --git a/src/client/app/desktop/views/pages/favorites.vue b/src/client/app/desktop/views/pages/favorites.vue index d908c08f7c..71d36cdf2b 100644 --- a/src/client/app/desktop/views/pages/favorites.vue +++ b/src/client/app/desktop/views/pages/favorites.vue @@ -4,7 +4,7 @@ <template v-for="favorite in favorites"> <mk-note-detail :note="favorite.note" :key="favorite.note.id"/> </template> - <a v-if="existMore" @click="more">さらに読み込む</a> + <a v-if="existMore" @click="more">%i18n:@more%</a> </main> </mk-ui> </template> diff --git a/src/client/app/desktop/views/pages/home-customize.vue b/src/client/app/desktop/views/pages/home-customize.vue index 8aa06be57f..da5f15bb69 100644 --- a/src/client/app/desktop/views/pages/home-customize.vue +++ b/src/client/app/desktop/views/pages/home-customize.vue @@ -6,7 +6,7 @@ import Vue from 'vue'; export default Vue.extend({ mounted() { - document.title = 'Misskey - ホームのカスタマイズ'; + document.title = 'Misskey - %i18n:@title%'; } }); </script> diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue index 1cc8d8a778..06c32776c9 100644 --- a/src/client/app/desktop/views/pages/messaging-room.vue +++ b/src/client/app/desktop/views/pages/messaging-room.vue @@ -21,10 +21,21 @@ export default Vue.extend({ $route: 'fetch' }, created() { + const applyBg = v => + document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important'); + + applyBg(this.$store.state.device.darkmode); + + this.unwatchDarkmode = this.$store.watch(s => { + return s.device.darkmode; + }, applyBg); + this.fetch(); }, - mounted() { - document.documentElement.style.background = '#fff'; + beforeDestroy() { + document.documentElement.style.removeProperty('background'); + document.documentElement.style.removeProperty('background-color'); // for safari's bug + this.unwatchDarkmode(); }, methods: { fetch() { @@ -50,6 +61,5 @@ export default Vue.extend({ flex 1 flex-direction column min-height 100% - background #fff </style> diff --git a/src/client/app/desktop/views/pages/selectdrive.vue b/src/client/app/desktop/views/pages/selectdrive.vue index 7a00896640..c846f2418f 100644 --- a/src/client/app/desktop/views/pages/selectdrive.vue +++ b/src/client/app/desktop/views/pages/selectdrive.vue @@ -29,7 +29,7 @@ export default Vue.extend({ } }, mounted() { - document.title = '%i18n:!@title%'; + document.title = '%i18n:@title%'; }, methods: { onSelected(file) { diff --git a/src/client/app/desktop/views/pages/user-list.users.vue b/src/client/app/desktop/views/pages/user-list.users.vue index 4236cdbb14..517fe89750 100644 --- a/src/client/app/desktop/views/pages/user-list.users.vue +++ b/src/client/app/desktop/views/pages/user-list.users.vue @@ -1,8 +1,8 @@ <template> <div> <mk-widget-container> - <template slot="header">%fa:users% ユーザー</template> - <button slot="func" title="ユーザーを追加" @click="add">%fa:plus%</button> + <template slot="header">%fa:users% %i18n:@users%</template> + <button slot="func" title="%i18n:@add-user%" @click="add">%fa:plus%</button> <div data-id="d0b63759-a822-4556-a5ce-373ab966e08a"> <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw% %i18n:common.loading%<mk-ellipsis/></p> @@ -48,7 +48,7 @@ export default Vue.extend({ methods: { add() { (this as any).apis.input({ - title: 'ユーザー名', + title: '%i18n:@username%', }).then(async username => { const user = await (this as any).api('users/show', { username diff --git a/src/client/app/desktop/views/pages/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue index 9c9840c190..576a285104 100644 --- a/src/client/app/desktop/views/pages/user/user.timeline.vue +++ b/src/client/app/desktop/views/pages/user/user.timeline.vue @@ -1,15 +1,15 @@ <template> <div class="timeline"> <header> - <span :data-active="mode == 'default'" @click="mode = 'default'">投稿</span> - <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span> - <span :data-active="mode == 'with-media'" @click="mode = 'with-media'">メディア</span> + <span :data-active="mode == 'default'" @click="mode = 'default'">%i18n:@default%</span> + <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">%i18n:@with-replies%</span> + <span :data-active="mode == 'with-media'" @click="mode = 'with-media'">%i18n:@with-media%</span> </header> <div class="loading" v-if="fetching"> <mk-ellipsis-icon/> </div> <mk-notes ref="timeline" :more="existMore ? more : null"> - <p class="empty" slot="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p> + <p class="empty" slot="empty">%fa:R comments%%i18n:@empty%</p> </mk-notes> </div> </template> diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue index 898b6b2179..91ad4b61c3 100644 --- a/src/client/app/desktop/views/pages/welcome.vue +++ b/src/client/app/desktop/views/pages/welcome.vue @@ -1,23 +1,16 @@ <template> <div class="mk-welcome"> + <button @click="dark"> + <template v-if="$store.state.device.darkmode">%fa:moon%</template> + <template v-else>%fa:R moon%</template> + </button> <main> - <div class="top"> - <div> - <div> - <h1>Share<br><span ref="share">Everything!</span><span class="cursor">_</span></h1> - <p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p> - <p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p> - <div class="users"> - <mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/> - </div> - </div> - <div> - <div> - <header>%fa:comments R% タイムライン<div><span></span><span></span><span></span></div></header> - <mk-welcome-timeline/> - </div> - </div> - </div> + <img :src="$store.state.device.darkmode ? 'assets/title-dark.svg' : 'assets/title.svg'" alt="Misskey"> + <p><button class="signup" @click="signup">%i18n:@signup-button%</button><button class="signin" @click="signin">%i18n:@signin-button%</button></p> + + <div class="tl"> + <header>%fa:comments R% %i18n:@timeline%<div><span></span><span></span><span></span></div></header> + <mk-welcome-timeline/> </div> </main> <mk-forkit/> @@ -28,11 +21,11 @@ </div> </footer> <modal name="signup" width="500px" height="auto" scrollable> - <header :class="$style.signupFormHeader">新規登録</header> + <header :class="$style.signupFormHeader">%i18n:@signup%</header> <mk-signup :class="$style.signupForm"/> </modal> <modal name="signin" width="500px" height="auto" scrollable> - <header :class="$style.signinFormHeader">ログイン</header> + <header :class="$style.signinFormHeader">%i18n:@signin%</header> <mk-signin :class="$style.signinForm"/> </modal> </div> @@ -42,64 +35,25 @@ import Vue from 'vue'; import { docsUrl, copyright, lang } from '../../../config'; -const shares = [ - 'Everything!', - 'Webpages', - 'Photos', - 'Interests', - 'Favorites' -]; - export default Vue.extend({ data() { return { aboutUrl: `${docsUrl}/${lang}/about`, - copyright, - users: [], - clock: null, - i: 0 + copyright }; }, - mounted() { - (this as any).api('users', { - sort: '+follower', - limit: 20 - }).then(users => { - this.users = users; - }); - - this.clock = setInterval(() => { - if (++this.i == shares.length) this.i = 0; - const speed = 70; - const text = (this.$refs.share as any).innerText; - for (let i = 0; i < text.length; i++) { - setTimeout(() => { - if (this.$refs.share) { - (this.$refs.share as any).innerText = text.substr(0, text.length - i); - } - }, i * speed) - } - setTimeout(() => { - const newText = shares[this.i]; - for (let i = 0; i <= newText.length; i++) { - setTimeout(() => { - if (this.$refs.share) { - (this.$refs.share as any).innerText = newText.substr(0, i); - } - }, i * speed) - } - }, text.length * speed); - }, 4000); - }, - beforeDestroy() { - clearInterval(this.clock); - }, methods: { signup() { this.$modal.show('signup'); }, signin() { this.$modal.show('signin'); + }, + dark() { + this.$store.commit('device/set', { + key: 'darkmode', + value: !this.$store.state.device.darkmode + }); } } }); @@ -115,161 +69,107 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -@import url('https://fonts.googleapis.com/css?family=Sarpanch:700') - -.mk-welcome +root(isDark) display flex flex-direction column flex 1 - $width = 1000px - background linear-gradient(to bottom, #1e1d65, #bd6659) - //background-image url('/assets/welcome-bg.svg') - background-size cover - background-position top center - - &:before - content "" - display block - position fixed - bottom 0 + > button + position absolute + z-index 1 + top 0 left 0 - width 100% - height 100% - background-image url('/assets/welcome-fg.svg') - background-size cover - background-position bottom center + padding 16px + font-size 18px + color isDark ? #fff : #555 > main - display flex flex 1 + padding 64px 0 0 0 + text-align center + color isDark ? #9aa4b3 : #555 - > .top - display flex - width 100% + > img + width 350px - > div - display flex - max-width $width + 64px - margin 0 auto - padding 80px 32px 0 32px + > p + margin 8px 0 + line-height 2em - > * - margin-bottom 48px + button + padding 8px 16px + font-size inherit - > div:first-child - margin-right 48px - color #fff - text-shadow 0 0 12px #172062 + .signup + color $theme-color + border solid 2px $theme-color + border-radius 4px - > h1 - margin 0 - font-weight bold - //font-variant small-caps - letter-spacing 12px - font-family 'Sarpanch', sans-serif - font-size 42px - line-height 48px + &:focus + box-shadow 0 0 0 3px rgba($theme-color, 0.2) - > .cursor - animation cursor 1s infinite linear both + &:hover + color $theme-color-foreground + background $theme-color - @keyframes cursor - 0% - opacity 1 - 50% - opacity 0 + &:active + color $theme-color-foreground + background darken($theme-color, 10%) + border-color darken($theme-color, 10%) - > p - margin 1em 0 - line-height 2em + .signin + &:hover + color isDark ? #fff : #000 - button - padding 8px 16px - font-size inherit + > .tl + margin 32px auto 0 auto + width 410px + text-align left + background isDark ? #313543 : #fff + border-radius 8px + box-shadow 0 8px 32px rgba(#000, 0.15) + overflow hidden - .signup - color $theme-color - border solid 2px $theme-color - border-radius 4px + > header + z-index 1 + padding 12px 16px + color isDark ? #e3e5e8 : #888d94 + box-shadow 0 1px 0px rgba(#000, 0.1) - &:focus - box-shadow 0 0 0 3px rgba($theme-color, 0.2) + > div + position absolute + top 0 + right 0 + padding inherit - &:hover - color $theme-color-foreground - background $theme-color + > span + display inline-block + height 11px + width 11px + margin-left 6px + border-radius 100% + vertical-align middle - &:active - color $theme-color-foreground - background darken($theme-color, 10%) - border-color darken($theme-color, 10%) + &:nth-child(1) + background #5BCC8B - .signin - &:hover - color #fff + &:nth-child(2) + background #E6BB46 - > .users - margin 16px 0 0 0 + &:nth-child(3) + background #DF7065 - > * - display inline-block - margin 4px - width 38px - height 38px - border-radius 6px - - > div:last-child - - > div - width 410px - background #fff - border-radius 8px - box-shadow 0 0 0 12px rgba(#000, 0.1) - overflow hidden - - > header - z-index 1 - padding 12px 16px - color #888d94 - box-shadow 0 1px 0px rgba(#000, 0.1) - - > div - position absolute - top 0 - right 0 - padding inherit - - > span - display inline-block - height 11px - width 11px - margin-left 6px - background #ccc - border-radius 100% - vertical-align middle - - &:nth-child(1) - background #5BCC8B - - &:nth-child(2) - background #E6BB46 - - &:nth-child(3) - background #DF7065 - - > .mk-welcome-timeline - max-height 350px - overflow auto + > .mk-welcome-timeline + max-height 350px + overflow auto > footer font-size 12px - color #949ea5 + color isDark ? #949ea5 : #737c82 > div - max-width $width margin 0 auto - padding 0 0 42px 0 + padding 64px text-align center > .c @@ -277,6 +177,12 @@ export default Vue.extend({ font-size 10px opacity 0.7 +.mk-welcome[data-darkmode] + root(true) + +.mk-welcome:not([data-darkmode]) + root(false) + </style> <style lang="stylus" module> diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue index 36fcc20636..7421a81102 100644 --- a/src/client/app/desktop/views/widgets/polls.vue +++ b/src/client/app/desktop/views/widgets/polls.vue @@ -4,7 +4,7 @@ <template slot="header">%fa:chart-pie%%i18n:@title%</template> <button slot="func" title="%i18n:@refresh%" @click="fetch">%fa:sync%</button> - <div class="mkw-polls--body" :data-darkmode="_darkmode_"> + <div class="mkw-polls--body" :data-darkmode="$store.state.device.darkmode"> <div class="poll" v-if="!fetching && poll != null"> <p v-if="poll.text"><router-link to="poll | notePage">{{ poll.text }}</router-link></p> <p v-if="!poll.text"><router-link to="poll | notePage">%fa:link%</router-link></p> diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue index 3b01ed034d..5af5b88e23 100644 --- a/src/client/app/desktop/views/widgets/profile.vue +++ b/src/client/app/desktop/views/widgets/profile.vue @@ -5,12 +5,12 @@ > <div class="banner" :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''" - title="クリックでバナー編集" + title="%i18n:@update-banner%" @click="os.apis.updateBanner" ></div> <mk-avatar class="avatar" :user="os.i" @click="os.apis.updateAvatar" - title="クリックでアバター編集" + title="%i18n:@update-avatar%" /> <router-link class="name" :to="os.i | userPage">{{ os.i | userName }}</router-link> <p class="username">@{{ os.i | acct }}</p> diff --git a/src/client/app/init.ts b/src/client/app/init.ts index 4908b73b23..560ab1a096 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -49,48 +49,6 @@ Vue.mixin({ } }); -// Dark/Light -const bus = new Vue(); -Vue.mixin({ - data() { - return { - _darkmode_: localStorage.getItem('darkmode') == 'true' - }; - }, - beforeCreate() { - // なぜか警告が出るので - this._darkmode_ = localStorage.getItem('darkmode') == 'true'; - }, - beforeDestroy() { - bus.$off('updated', this._onDarkmodeUpdated_); - }, - mounted() { - this._onDarkmodeUpdated_(this._darkmode_); - bus.$on('updated', this._onDarkmodeUpdated_); - }, - methods: { - _updateDarkmode_(v) { - localStorage.setItem('darkmode', v.toString()); - if (v) { - document.documentElement.setAttribute('data-darkmode', 'true'); - } else { - document.documentElement.removeAttribute('data-darkmode'); - } - bus.$emit('updated', v); - }, - _onDarkmodeUpdated_(v) { - if (!this.$el || !this.$el.setAttribute) return; - if (v) { - this.$el.setAttribute('data-darkmode', 'true'); - } else { - this.$el.removeAttribute('data-darkmode'); - } - this._darkmode_ = v; - this.$forceUpdate(); - } - } -}); - /** * APP ENTRY POINT! */ @@ -113,7 +71,7 @@ html.setAttribute('lang', lang); const head = document.getElementsByTagName('head')[0]; const meta = document.createElement('meta'); meta.setAttribute('name', 'description'); -meta.setAttribute('content', '%i18n:!common.misskey%'); +meta.setAttribute('content', '%i18n:common.misskey%'); head.appendChild(meta); //#endregion @@ -141,13 +99,52 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API) const launch = (router: VueRouter, api?: (os: MiOS) => API) => { os.apis = api ? api(os) : null; + //#region Dark/Light + Vue.mixin({ + data() { + return { + _unwatchDarkmode_: null + }; + }, + mounted() { + const apply = v => { + if (this.$el.setAttribute == null) return; + if (v) { + this.$el.setAttribute('data-darkmode', 'true'); + } else { + this.$el.removeAttribute('data-darkmode'); + } + }; + + apply(os.store.state.device.darkmode); + + this._unwatchDarkmode_ = os.store.watch(s => { + return s.device.darkmode; + }, apply); + }, + beforeDestroy() { + this._unwatchDarkmode_(); + } + }); + + os.store.watch(s => { + return s.device.darkmode; + }, v => { + if (v) { + document.documentElement.setAttribute('data-darkmode', 'true'); + } else { + document.documentElement.removeAttribute('data-darkmode'); + } + }); + //#endregion + Vue.mixin({ data() { return { os, api: os.api, apis: os.apis, - clientSettings: os.store.state.settings.data + clientSettings: os.store.state.settings }; } }); @@ -173,7 +170,7 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API) } //#region 更新チェック - const preventUpdate = localStorage.getItem('preventUpdate') == 'true'; + const preventUpdate = os.store.state.device.preventUpdate; if (!preventUpdate) { setTimeout(() => { checkForUpdate(os); diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts index 2373b0d8d2..a5a38a5414 100644 --- a/src/client/app/mios.ts +++ b/src/client/app/mios.ts @@ -98,14 +98,7 @@ export default class MiOS extends EventEmitter { * Whether is debug mode */ public get debug() { - return localStorage.getItem('debug') == 'true'; - } - - /** - * Whether enable sounds - */ - public get isEnableSounds() { - return localStorage.getItem('enableSounds') == 'true'; + return this.store ? this.store.state.device.debug : false; } public store: ReturnType<typeof initStore>; @@ -435,12 +428,8 @@ export default class MiOS extends EventEmitter { }); }); - // Whether use raw version script - const raw = (localStorage.getItem('useRawScript') == 'true' && this.debug) - || process.env.NODE_ENV != 'production'; - // The path of service worker script - const sw = `/sw.${version}.${lang}.${raw ? 'raw' : 'min'}.js`; + const sw = `/sw.${version}.${lang}.js`; // Register service worker navigator.serviceWorker.register(sw).then(registration => { @@ -471,8 +460,7 @@ export default class MiOS extends EventEmitter { }; const promise = new Promise((resolve, reject) => { - const viaStream = this.stream && this.stream.hasConnection && - (localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true); + const viaStream = this.stream && this.stream.hasConnection && this.store.state.device.apiViaStream; if (viaStream) { const stream = this.stream.borrow(); diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts index 427c177a14..5418aac090 100644 --- a/src/client/app/mobile/script.ts +++ b/src/client/app/mobile/script.ts @@ -5,7 +5,7 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; -import { MdCard, MdButton, MdField, MdMenu, MdList, MdSwitch } from 'vue-material/dist/components'; +import { MdCard, MdButton, MdField, MdMenu, MdList, MdSwitch, MdSubheader, MdDialog, MdDialogAlert, MdRadio } from 'vue-material/dist/components'; import 'vue-material/dist/vue-material.min.css'; import 'vue-material/dist/theme/default.css'; @@ -37,7 +37,6 @@ import MkSearch from './views/pages/search.vue'; import MkFollowers from './views/pages/followers.vue'; import MkFollowing from './views/pages/following.vue'; import MkSettings from './views/pages/settings.vue'; -import MkProfileSetting from './views/pages/profile-setting.vue'; import MkOthello from './views/pages/othello.vue'; Vue.use(MdCard); @@ -46,6 +45,10 @@ Vue.use(MdField); Vue.use(MdMenu); Vue.use(MdList); Vue.use(MdSwitch); +Vue.use(MdSubheader); +Vue.use(MdDialog); +Vue.use(MdDialogAlert); +Vue.use(MdRadio); /** * init @@ -67,8 +70,7 @@ init((launch) => { routes: [ { path: '/', name: 'index', component: MkIndex }, { path: '/signup', name: 'signup', component: MkSignup }, - { path: '/i/settings', component: MkSettings }, - { path: '/i/settings/profile', component: MkProfileSetting }, + { path: '/i/settings', name: 'settings', component: MkSettings }, { path: '/i/notifications', name: 'notifications', component: MkNotifications }, { path: '/i/widgets', name: 'widgets', component: MkWidgets }, { path: '/i/messaging', name: 'messaging', component: MkMessaging }, diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue index ef3432a3ec..8e35e6c88b 100644 --- a/src/client/app/mobile/views/components/drive.vue +++ b/src/client/app/mobile/views/components/drive.vue @@ -32,7 +32,7 @@ <div class="files" v-if="files.length > 0"> <x-file v-for="file in files" :key="file.id" :file="file"/> <button class="more" v-if="moreFiles" @click="fetchMoreFiles"> - {{ fetchingMoreFiles ? '%i18n:!common.loading%' : '%i18n:!@load-more%' }} + {{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:@load-more%' }} </button> </div> <div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching"> diff --git a/src/client/app/mobile/views/components/follow-button.vue b/src/client/app/mobile/views/components/follow-button.vue index 5d6b8ebf84..a6b5cf0556 100644 --- a/src/client/app/mobile/views/components/follow-button.vue +++ b/src/client/app/mobile/views/components/follow-button.vue @@ -7,7 +7,7 @@ <template v-if="!wait && user.isFollowing">%fa:minus%</template> <template v-if="!wait && !user.isFollowing">%fa:plus%</template> <template v-if="wait">%fa:spinner .pulse .fw%</template> - {{ user.isFollowing ? '%i18n:!@unfollow%' : '%i18n:!@follow%' }} + {{ user.isFollowing ? '%i18n:@unfollow%' : '%i18n:@follow%' }} </button> </template> diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue index 9e0f8e5f7e..c2f9c66e84 100644 --- a/src/client/app/mobile/views/components/media-image.vue +++ b/src/client/app/mobile/views/components/media-image.vue @@ -17,9 +17,17 @@ export default Vue.extend({ }, computed: { style(): any { + let url = `url(${this.image.url}?thumbnail)`; + + if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) { + url = null; + } else if (this.raw || this.$store.state.device.loadRawImages) { + url = `url(${this.image.url})`; + } + return { 'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent', - 'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)` + 'background-image': url }; } } diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue index c6664a91da..244dbb6c03 100644 --- a/src/client/app/mobile/views/components/note-detail.vue +++ b/src/client/app/mobile/views/components/note-detail.vue @@ -2,15 +2,15 @@ <div class="mk-note-detail"> <button class="more" - v-if="p.reply && p.reply.replyId && context.length == 0" - @click="fetchContext" - :disabled="fetchingContext" + v-if="p.reply && p.reply.replyId && conversation.length == 0" + @click="fetchConversation" + :disabled="conversationFetching" > - <template v-if="!contextFetching">%fa:ellipsis-v%</template> - <template v-if="contextFetching">%fa:spinner .pulse%</template> + <template v-if="!conversationFetching">%fa:ellipsis-v%</template> + <template v-if="conversationFetching">%fa:spinner .pulse%</template> </button> - <div class="context"> - <x-sub v-for="note in context" :key="note.id" :note="note"/> + <div class="conversation"> + <x-sub v-for="note in conversation" :key="note.id" :note="note"/> </div> <div class="reply-to" v-if="p.reply"> <x-sub :note="p.reply"/> @@ -99,8 +99,8 @@ export default Vue.extend({ data() { return { - context: [], - contextFetching: false, + conversation: [], + conversationFetching: false, replies: [] }; }, @@ -166,14 +166,14 @@ export default Vue.extend({ methods: { fetchContext() { - this.contextFetching = true; + this.conversationFetching = true; - // Fetch context - (this as any).api('notes/context', { + // Fetch conversation + (this as any).api('notes/conversation', { noteId: this.p.replyId - }).then(context => { - this.contextFetching = false; - this.context = context.reverse(); + }).then(conversation => { + this.conversationFetching = false; + this.conversation = conversation.reverse(); }); }, reply() { @@ -245,7 +245,7 @@ root(isDark) &:disabled color #ccc - > .context + > .conversation > * border-bottom 1px solid isDark ? #1c2023 : #eef0f2 diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue index b55cad792d..8fa57768e0 100644 --- a/src/client/app/mobile/views/components/note-preview.vue +++ b/src/client/app/mobile/views/components/note-preview.vue @@ -1,9 +1,13 @@ <template> -<div class="mk-note-preview"> - <mk-avatar class="avatar" :user="note.user"/> +<div class="mk-note-preview" :class="{ smart: $store.state.device.postStyle == 'smart' }"> + <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/> <div class="main"> <header> + <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/> <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span class="is-admin" v-if="note.user.isAdmin">admin</span> + <span class="is-bot" v-if="note.user.isBot">bot</span> + <span class="is-cat" v-if="note.user.isCat">cat</span> <span class="username"><mk-acct :user="note.user"/></span> <router-link class="time" :to="note | notePage"> <mk-time :time="note.createdAt"/> @@ -35,6 +39,13 @@ root(isDark) display block clear both + &.smart + > .main + width 100% + + > header + align-items center + > .avatar display block float left @@ -53,6 +64,13 @@ root(isDark) margin-bottom 4px white-space nowrap + > .avatar + flex-shrink 0 + margin-right 8px + width 18px + height 18px + border-radius 100% + > .name display block margin 0 .5em 0 0 @@ -65,8 +83,19 @@ root(isDark) text-decoration none text-overflow ellipsis - &:hover - text-decoration underline + > .is-admin + > .is-bot + > .is-cat + margin 0 0.5em 0 0 + padding 1px 6px + font-size 10px + color isDark ? #758188 : #aaa + border solid 1px isDark ? #57616f : #ddd + border-radius 3px + + &.is-admin + border-color isDark ? #d42c41 : #f56a7b + color isDark ? #d42c41 : #f56a7b > .username margin 0 .5em 0 0 diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue index 2fb3b2ffcc..149a78ecde 100644 --- a/src/client/app/mobile/views/components/note.sub.vue +++ b/src/client/app/mobile/views/components/note.sub.vue @@ -1,9 +1,13 @@ <template> -<div class="sub"> - <mk-avatar class="avatar" :user="note.user"/> +<div class="sub" :class="{ smart: $store.state.device.postStyle == 'smart' }"> + <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/> <div class="main"> <header> + <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/> <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span class="is-admin" v-if="note.user.isAdmin">admin</span> + <span class="is-bot" v-if="note.user.isBot">bot</span> + <span class="is-cat" v-if="note.user.isCat">cat</span> <span class="username"><mk-acct :user="note.user"/></span> <div class="info"> <span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span> @@ -42,6 +46,13 @@ root(isDark) @media (min-width 600px) padding 24px 32px + &.smart + > .main + width 100% + + > header + align-items center + &:after content "" display block @@ -73,6 +84,13 @@ root(isDark) margin-bottom 2px white-space nowrap + > .avatar + flex-shrink 0 + margin-right 8px + width 18px + height 18px + border-radius 100% + > .name display block margin 0 0.5em 0 0 @@ -88,6 +106,20 @@ root(isDark) &:hover text-decoration underline + > .is-admin + > .is-bot + > .is-cat + margin 0 0.5em 0 0 + padding 1px 5px + font-size 10px + color isDark ? #758188 : #aaa + border solid 1px isDark ? #57616f : #ddd + border-radius 3px + + &.is-admin + border-color isDark ? #d42c41 : #f56a7b + color isDark ? #d42c41 : #f56a7b + > .username text-align left margin 0 diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue index 83a957cfbd..2004263d22 100644 --- a/src/client/app/mobile/views/components/note.vue +++ b/src/client/app/mobile/views/components/note.vue @@ -1,22 +1,25 @@ <template> -<div class="note" :class="{ renote: isRenote }"> +<div class="note" :class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart' }"> <div class="reply-to" v-if="p.reply && (!os.isSignedIn || clientSettings.showReplyTarget)"> <x-sub :note="p.reply"/> </div> <div class="renote" v-if="isRenote"> <mk-avatar class="avatar" :user="note.user"/> %fa:retweet% - <span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span> + <span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span> <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> - <span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span> + <span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span> <mk-time :time="note.createdAt"/> </div> <article> - <mk-avatar class="avatar" :user="p.user"/> + <mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle != 'smart'"/> <div class="main"> <header> + <mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle == 'smart'"/> <router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> - <span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span> + <span class="is-admin" v-if="p.user.isAdmin">admin</span> + <span class="is-bot" v-if="p.user.isBot">bot</span> + <span class="is-cat" v-if="p.user.isCat">cat</span> <span class="username"><mk-acct :user="p.user"/></span> <div class="info"> <span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span> @@ -262,6 +265,15 @@ root(isDark) @media (min-width 500px) font-size 16px + &.smart + > article + > .main + width 100% + + > header + align-items center + margin-bottom 4px + > .renote display flex align-items center @@ -278,12 +290,17 @@ root(isDark) padding 16px 32px .avatar + flex-shrink 0 display inline-block - width 28px - height 28px + width 20px + height 20px margin 0 8px 0 0 border-radius 6px + @media (min-width 500px) + width 28px + height 28px + [data-fa] margin-right 4px @@ -352,21 +369,26 @@ root(isDark) @media (min-width 500px) margin-bottom 2px + > .avatar + flex-shrink 0 + margin-right 8px + width 20px + height 20px + border-radius 100% + > .name display block margin 0 0.5em 0 0 padding 0 overflow hidden color isDark ? #fff : #627079 - font-size 1em font-weight bold text-decoration none text-overflow ellipsis - &:hover - text-decoration underline - + > .is-admin > .is-bot + > .is-cat margin 0 0.5em 0 0 padding 1px 6px font-size 12px @@ -374,6 +396,10 @@ root(isDark) border solid 1px isDark ? #57616f : #ddd border-radius 3px + &.is-admin + border-color isDark ? #d42c41 : #f56a7b + color isDark ? #d42c41 : #f56a7b + > .username margin 0 0.5em 0 0 overflow hidden diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue index 8ab66940c4..6bb9e9bb2c 100644 --- a/src/client/app/mobile/views/components/notifications.vue +++ b/src/client/app/mobile/views/components/notifications.vue @@ -12,7 +12,7 @@ <button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template> - {{ fetchingMoreNotifications ? '%i18n:!common.loading%' : '%i18n:!@more%' }} + {{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }} </button> <p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p> diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue index 0bb498e5d7..b3b5ffd502 100644 --- a/src/client/app/mobile/views/components/post-form.vue +++ b/src/client/app/mobile/views/components/post-form.vue @@ -20,7 +20,7 @@ <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%' : renote ? '%i18n:!@renote-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"> diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue index ec42dbc99d..aa469bd1c8 100644 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ b/src/client/app/mobile/views/components/ui.nav.vue @@ -28,8 +28,8 @@ <li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li> </ul> <ul> - <li><router-link to="/i/settings">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li> - <li @click="dark"><p><template v-if="_darkmode_">%fa:moon%</template><template v-else>%fa:R moon%</template><span>ダークモード</span></p></li> + <li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li> + <li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>ダークモード</span></p></li> </ul> </div> <a :href="aboutUrl"><p class="about">%i18n:@about%</p></a> @@ -94,7 +94,7 @@ export default Vue.extend({ }, methods: { search() { - const query = window.prompt('%i18n:!@search%'); + const query = window.prompt('%i18n:@search%'); if (query == null || query == '') return; this.$router.push('/search?q=' + encodeURIComponent(query)); }, @@ -117,7 +117,10 @@ export default Vue.extend({ this.hasGameInvitations = false; }, dark() { - (this as any)._updateDarkmode_(!(this as any)._darkmode_); + this.$store.commit('device/set', { + key: 'darkmode', + value: !this.$store.state.device.darkmode + }); } } }); diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue index 52c82115bf..808ee72402 100644 --- a/src/client/app/mobile/views/components/user-card.vue +++ b/src/client/app/mobile/views/components/user-card.vue @@ -1,9 +1,7 @@ <template> <div class="mk-user-card"> <header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"> - <a :href="user | userPage"> - <img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/> - </a> + <mk-avatar class="avatar" :user="user"/> </header> <a class="name" :href="user | userPage" target="_blank">{{ user | userName }}</a> <p class="username"><mk-acct :user="user"/></p> @@ -35,15 +33,14 @@ export default Vue.extend({ background-position center border-radius 8px 8px 0 0 - > a - > img - position absolute - top 20px - left calc(50% - 40px) - width 80px - height 80px - border solid 2px #fff - border-radius 8px + > .avatar + position absolute + top 20px + left calc(50% - 40px) + width 80px + height 80px + border solid 2px #fff + border-radius 8px > .name display block diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue index 3ceb876596..aca6f783b8 100644 --- a/src/client/app/mobile/views/components/user-timeline.vue +++ b/src/client/app/mobile/views/components/user-timeline.vue @@ -3,7 +3,7 @@ <mk-notes ref="timeline" :more="existMore ? more : null"> <div slot="empty"> %fa:R comments% - {{ withMedia ? '%i18n:!@no-notes-with-media%' : '%i18n:!@no-notes%' }} + {{ withMedia ? '%i18n:@no-notes-with-media%' : '%i18n:@no-notes%' }} </div> </mk-notes> </div> diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue index 33ade94e35..dfb9c62142 100644 --- a/src/client/app/mobile/views/pages/followers.vue +++ b/src/client/app/mobile/views/pages/followers.vue @@ -2,7 +2,7 @@ <mk-ui> <template slot="header" v-if="!fetching"> <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""> - {{ '%i18n:!@followers-of%'.replace('{}', name) }} + {{ '%i18n:@followers-of%'.replace('{}', name) }} </template> <mk-users-list v-if="!fetching" @@ -49,7 +49,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; - document.title = '%i18n:!@followers-of%'.replace('{}', this.name) + ' | Misskey'; + document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | Misskey'; }); }, onLoaded() { diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue index c6d6d44281..35461ea2fc 100644 --- a/src/client/app/mobile/views/pages/following.vue +++ b/src/client/app/mobile/views/pages/following.vue @@ -2,7 +2,7 @@ <mk-ui> <template slot="header" v-if="!fetching"> <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""> - {{ '%i18n:!@following-of%'.replace('{}', name) }} + {{ '%i18n:@following-of%'.replace('{}', name) }} </template> <mk-users-list v-if="!fetching" @@ -48,7 +48,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; - document.title = '%i18n:!@followers-of%'.replace('{}', this.name) + ' | Misskey'; + document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | Misskey'; }); }, onLoaded() { diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index ad6d5ed408..5701ff03d5 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -2,9 +2,9 @@ <mk-ui> <span slot="header" @click="showNav = true"> <span> - <span v-if="src == 'home'">%fa:home%ホーム</span> - <span v-if="src == 'local'">%fa:R comments%ローカル</span> - <span v-if="src == 'global'">%fa:globe%グローバル</span> + <span v-if="src == 'home'">%fa:home%%i18n:@home%</span> + <span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span> + <span v-if="src == 'global'">%fa:globe%%i18n:@global%</span> <span v-if="src.startsWith('list')">%fa:list%{{ list.title }}</span> </span> <span style="margin-left:8px"> @@ -17,14 +17,14 @@ <button @click="fn">%fa:pencil-alt%</button> </template> - <main :data-darkmode="_darkmode_"> + <main :data-darkmode="$store.state.device.darkmode"> <div class="nav" v-if="showNav"> <div class="bg" @click="showNav = false"></div> <div class="body"> <div> - <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span> - <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span> - <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% グローバル</span> + <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> + <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span> + <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> <template v-if="lists"> <span v-for="l in lists" :data-active="src == 'list:' + l.id" @click="src = 'list:' + l.id; list = l" :key="l.id">%fa:list% {{ l.title }}</span> </template> diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue index c26a9b735e..8b82b03fb9 100644 --- a/src/client/app/mobile/views/pages/messaging-room.vue +++ b/src/client/app/mobile/views/pages/messaging-room.vue @@ -16,16 +16,30 @@ export default Vue.extend({ data() { return { fetching: true, - user: null + user: null, + unwatchDarkmode: null }; }, watch: { $route: 'fetch' }, created() { - document.documentElement.style.background = '#fff'; + const applyBg = v => + document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important'); + + applyBg(this.$store.state.device.darkmode); + + this.unwatchDarkmode = this.$store.watch(s => { + return s.device.darkmode; + }, applyBg); + this.fetch(); }, + beforeDestroy() { + document.documentElement.style.removeProperty('background'); + document.documentElement.style.removeProperty('background-color'); // for safari's bug + this.unwatchDarkmode(); + }, methods: { fetch() { this.fetching = true; @@ -39,4 +53,3 @@ export default Vue.extend({ } }); </script> - diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue index cc328e5a1c..057470efd9 100644 --- a/src/client/app/mobile/views/pages/messaging.vue +++ b/src/client/app/mobile/views/pages/messaging.vue @@ -12,7 +12,6 @@ import getAcct from '../../../../../acct/render'; export default Vue.extend({ mounted() { document.title = 'Misskey %i18n:@messaging%'; - document.documentElement.style.background = '#fff'; }, methods: { navigate(user) { diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue index d0c0fe9535..2e98201caa 100644 --- a/src/client/app/mobile/views/pages/notifications.vue +++ b/src/client/app/mobile/views/pages/notifications.vue @@ -21,7 +21,7 @@ export default Vue.extend({ }, methods: { fn() { - const ok = window.confirm('%i18n:!@read-all%'); + const ok = window.confirm('%i18n:@read-all%'); if (!ok) return; (this as any).api('notifications/markAsRead_all'); diff --git a/src/client/app/mobile/views/pages/profile-setting.vue b/src/client/app/mobile/views/pages/profile-setting.vue deleted file mode 100644 index 7048cdef31..0000000000 --- a/src/client/app/mobile/views/pages/profile-setting.vue +++ /dev/null @@ -1,225 +0,0 @@ -<template> -<mk-ui> - <span slot="header">%fa:user%%i18n:@title%</span> - <div :class="$style.content"> - <p>%fa:info-circle%%i18n:@will-be-published%</p> - <div :class="$style.form"> - <div :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=1024)` : ''" @click="setBanner"> - <img :src="`${os.i.avatarUrl}?thumbnail&size=200`" alt="avatar" @click="setAvatar"/> - </div> - <label> - <p>%i18n:@name%</p> - <input v-model="name" type="text"/> - </label> - <label> - <p>%i18n:@location%</p> - <input v-model="location" type="text"/> - </label> - <label> - <p>%i18n:@description%</p> - <textarea v-model="description"></textarea> - </label> - <label> - <p>%i18n:@birthday%</p> - <input v-model="birthday" type="date"/> - </label> - <label> - <p>%i18n:@avatar%</p> - <button @click="setAvatar" :disabled="avatarSaving">%i18n:@set-avatar%</button> - </label> - <label> - <p>%i18n:@banner%</p> - <button @click="setBanner" :disabled="bannerSaving">%i18n:@set-banner%</button> - </label> - </div> - <button :class="$style.save" @click="save" :disabled="saving">%fa:check%%i18n:@save%</button> - </div> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - name: null, - location: null, - description: null, - birthday: null, - avatarSaving: false, - bannerSaving: false, - saving: false - }; - }, - created() { - this.name = (this as any).os.i.name || ''; - this.location = (this as any).os.i.profile.location; - this.description = (this as any).os.i.description; - this.birthday = (this as any).os.i.profile.birthday; - }, - mounted() { - document.title = 'Misskey | %i18n:@title%'; - }, - methods: { - setAvatar() { - (this as any).apis.chooseDriveFile({ - multiple: false - }).then(file => { - this.avatarSaving = true; - - (this as any).api('i/update', { - avatarId: file.id - }).then(() => { - this.avatarSaving = false; - alert('%i18n:!@avatar-saved%'); - }); - }); - }, - setBanner() { - (this as any).apis.chooseDriveFile({ - multiple: false - }).then(file => { - this.bannerSaving = true; - - (this as any).api('i/update', { - bannerId: file.id - }).then(() => { - this.bannerSaving = false; - alert('%i18n:!@banner-saved%'); - }); - }); - }, - save() { - this.saving = true; - - (this as any).api('i/update', { - name: this.name || null, - location: this.location || null, - description: this.description || null, - birthday: this.birthday || null - }).then(() => { - this.saving = false; - alert('%i18n:!@saved%'); - }); - } - } -}); -</script> - -<style lang="stylus" module> -@import '~const.styl' - -.content - margin 8px auto - max-width 500px - width calc(100% - 16px) - - @media (min-width 500px) - margin 16px auto - width calc(100% - 32px) - - > p - display block - margin 0 0 8px 0 - padding 12px 16px - font-size 14px - color #79d4e6 - border solid 1px #71afbb - //color #276f86 - //background #f8ffff - //border solid 1px #a9d5de - border-radius 8px - - > [data-fa] - margin-right 6px - -.form - position relative - background #fff - box-shadow 0 0 0 1px rgba(#000, 0.2) - border-radius 8px - - &:before - content "" - display block - position absolute - bottom -20px - left calc(50% - 10px) - border-top solid 10px rgba(#000, 0.2) - border-right solid 10px transparent - border-bottom solid 10px transparent - border-left solid 10px transparent - - &:after - content "" - display block - position absolute - bottom -16px - left calc(50% - 8px) - border-top solid 8px #fff - border-right solid 8px transparent - border-bottom solid 8px transparent - border-left solid 8px transparent - - > div - height 128px - background-color #e4e4e4 - background-size cover - background-position center - border-radius 8px 8px 0 0 - - > img - position absolute - top 25px - left calc(50% - 40px) - width 80px - height 80px - border solid 2px #fff - border-radius 8px - - > label - display block - margin 0 - padding 16px - border-bottom solid 1px #eee - - &:last-of-type - border none - - > p:first-child - display block - margin 0 - padding 0 0 4px 0 - font-weight bold - color #2f3c42 - - > input[type="text"] - > textarea - display block - width 100% - padding 12px - font-size 16px - color #192427 - border solid 2px #ddd - border-radius 4px - - > textarea - min-height 80px - -.save - display block - margin 8px 0 0 0 - padding 16px - width 100% - font-size 16px - color $theme-color-foreground - background $theme-color - border-radius 8px - - &:disabled - opacity 0.7 - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue index f038a6f81f..9850fbcfb4 100644 --- a/src/client/app/mobile/views/pages/search.vue +++ b/src/client/app/mobile/views/pages/search.vue @@ -3,7 +3,7 @@ <span slot="header">%fa:search% {{ q }}</span> <main v-if="!fetching"> <mk-notes :class="$style.notes" :notes="notes"> - <span v-if="notes.length == 0">{{ '%i18n:!@empty%'.replace('{}', q) }}</span> + <span v-if="notes.length == 0">{{ '%i18n:@empty%'.replace('{}', q) }}</span> <button v-if="existMore" @click="more" :disabled="fetching" slot="tail"> <span v-if="!fetching">%i18n:@load-more%</span> <span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span> diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue index d730e4fcff..1a162b346c 100644 --- a/src/client/app/mobile/views/pages/selectdrive.vue +++ b/src/client/app/mobile/views/pages/selectdrive.vue @@ -25,7 +25,7 @@ export default Vue.extend({ } }, mounted() { - document.title = '%i18n:!@title%'; + document.title = '%i18n:@title%'; }, methods: { onSelected(file) { diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index b16860d62c..3bb25f88f8 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -2,13 +2,13 @@ <mk-ui> <span slot="header">%fa:cog%%i18n:@settings%</span> <main> - <p v-html="'%i18n:!@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p> + <p v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p> <div> <x-profile/> - <md-card class="md-layout-item md-size-50 md-small-size-100"> + <md-card> <md-card-header> - <div class="md-title">%i18n:@design%</div> + <div class="md-title">%fa:palette% %i18n:@design%</div> </md-card-header> <md-card-content> @@ -19,6 +19,110 @@ <div> <md-switch v-model="clientSettings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</md-switch> </div> + + <div> + <div class="md-body-2">%i18n:@timeline%</div> + + <div> + <md-switch v-model="clientSettings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</md-switch> + </div> + + <div> + <md-switch v-model="clientSettings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</md-switch> + </div> + + <div> + <md-switch v-model="clientSettings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</md-switch> + </div> + </div> + + <div> + <div class="md-body-2">%i18n:@post-style%</div> + + <md-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</md-radio> + <md-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</md-radio> + </div> + </md-card-content> + </md-card> + + <md-card> + <md-card-header> + <div class="md-title">%fa:cog% %i18n:@behavior%</div> + </md-card-header> + + <md-card-content> + <div> + <md-switch v-model="clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</md-switch> + </div> + + <div> + <md-switch v-model="clientSettings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</md-switch> + </div> + + <div> + <md-switch v-model="loadRawImages">%i18n:@load-raw-images%</md-switch> + </div> + + <div> + <md-switch v-model="clientSettings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</md-switch> + </div> + + <div> + <md-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</md-switch> + </div> + </md-card-content> + </md-card> + + <md-card> + <md-card-header> + <div class="md-title">%fa:language% %i18n:@lang%</div> + </md-card-header> + + <md-card-content> + <md-field> + <md-select v-model="lang" placeholder="%i18n:@auto%"> + <md-optgroup label="%i18n:@recommended%"> + <md-option value="">%i18n:@auto%</md-option> + </md-optgroup> + + <md-optgroup label="%i18n:@specify-language%"> + <md-option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</md-option> + </md-optgroup> + </md-select> + </md-field> + <span class="md-helper-text">%fa:info-circle% %i18n:@lang-tip%</span> + </md-card-content> + </md-card> + + <md-card> + <md-card-header> + <div class="md-title">%fa:B twitter% %i18n:@twitter%</div> + </md-card-header> + + <md-card-content> + <p class="account" v-if="os.i.twitter"><a :href="`https://twitter.com/${os.i.twitter.screenName}`" target="_blank">@{{ os.i.twitter.screenName }}</a></p> + <p> + <a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ os.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a> + <span v-if="os.i.twitter"> or </span> + <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.twitter">%i18n:@twitter-disconnect%</a> + </p> + </md-card-content> + </md-card> + + <md-card> + <md-card-header> + <div class="md-title">%fa:sync-alt% %i18n:@update%</div> + </md-card-header> + + <md-card-content> + <div>%i18n:@version% <i>{{ version }}</i></div> + <template v-if="latestVersion !== undefined"> + <div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div> + </template> + <md-button class="md-raised md-primary" @click="checkForUpdate" :disabled="checkingForUpdate"> + <template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template> + <template v-else>%i18n:@check-for-updates%</template> + </md-button> </md-card-content> </md-card> </div> @@ -29,7 +133,8 @@ <script lang="ts"> import Vue from 'vue'; -import { version, codename } from '../../../config'; +import { apiUrl, version, codename, langs } from '../../../config'; +import checkForUpdate from '../../../common/scripts/check-for-update'; import XProfile from './settings/settings.profile.vue'; @@ -40,22 +145,44 @@ export default Vue.extend({ data() { return { + apiUrl, version, codename, - darkmode: localStorage.getItem('darkmode') == 'true' + langs, + latestVersion: undefined, + checkingForUpdate: false }; }, computed: { name(): string { return Vue.filter('userName')((this as any).os.i); - } - }, + }, - watch: { - darkmode() { - (this as any)._updateDarkmode_(this.darkmode); - } + darkmode: { + get() { return this.$store.state.device.darkmode; }, + set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); } + }, + + postStyle: { + get() { return this.$store.state.device.postStyle; }, + set(value) { this.$store.commit('device/set', { key: 'postStyle', value }); } + }, + + lightmode: { + get() { return this.$store.state.device.lightmode; }, + set(value) { this.$store.commit('device/set', { key: 'lightmode', value }); } + }, + + loadRawImages: { + get() { return this.$store.state.device.loadRawImages; }, + set(value) { this.$store.commit('device/set', { key: 'loadRawImages', value }); } + }, + + lang: { + get() { return this.$store.state.device.lang; }, + set(value) { this.$store.commit('device/set', { key: 'lang', value }); } + }, }, mounted() { @@ -67,19 +194,83 @@ export default Vue.extend({ (this as any).os.signout(); }, + onChangeFetchOnScroll(v) { + this.$store.dispatch('settings/set', { + key: 'fetchOnScroll', + value: v + }); + }, + + onChangeDisableViaMobile(v) { + this.$store.dispatch('settings/set', { + key: 'disableViaMobile', + value: v + }); + }, + + onChangeLoadRemoteMedia(v) { + this.$store.dispatch('settings/set', { + key: 'loadRemoteMedia', + value: v + }); + }, + onChangeCircleIcons(v) { this.$store.dispatch('settings/set', { key: 'circleIcons', value: v }); + }, + + onChangeShowReplyTarget(v) { + this.$store.dispatch('settings/set', { + key: 'showReplyTarget', + value: v + }); + }, + + onChangeShowMyRenotes(v) { + this.$store.dispatch('settings/set', { + key: 'showMyRenotes', + value: v + }); + }, + + onChangeShowRenotedMyNotes(v) { + this.$store.dispatch('settings/set', { + key: 'showRenotedMyNotes', + value: v + }); + }, + + checkForUpdate() { + this.checkingForUpdate = true; + checkForUpdate((this as any).os, true, true).then(newer => { + this.checkingForUpdate = false; + this.latestVersion = newer; + if (newer == null) { + (this as any).apis.dialog({ + title: '%i18n:@no-updates%', + text: '%i18n:@no-updates-desc%' + }); + } else { + (this as any).apis.dialog({ + title: '%i18n:@update-available%', + text: '%i18n:@update-available-desc%' + }); + } + }); } } }); </script> <style lang="stylus" scoped> -main +root(isDark) padding 0 16px + margin 0 auto + max-width 500px + width 100% > div > * @@ -89,57 +280,12 @@ main display block margin 24px text-align center - color #cad2da + color isDark ? #cad2da : #a2a9b1 - > ul - $radius = 8px +main[data-darkmode] + root(true) - display block - margin 16px auto - padding 0 - max-width 500px - width calc(100% - 32px) - list-style none - background #fff - border solid 1px rgba(#000, 0.2) - border-radius $radius - - > li - display block - border-bottom solid 1px #ddd - - &:hover - background rgba(#000, 0.1) - - &:first-child - border-top-left-radius $radius - border-top-right-radius $radius - - &:last-child - border-bottom-left-radius $radius - border-bottom-right-radius $radius - border-bottom none - - > a - $height = 48px - - display block - position relative - padding 0 16px - line-height $height - color #4d635e - - > [data-fa]:nth-of-type(1) - margin-right 4px - - > [data-fa]:nth-of-type(2) - display block - position absolute - top 0 - right 8px - z-index 1 - padding 0 20px - font-size 1.2em - line-height $height +main:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/mobile/views/pages/settings/settings.profile.vue b/src/client/app/mobile/views/pages/settings/settings.profile.vue index 6b5d07cce9..c16c44e133 100644 --- a/src/client/app/mobile/views/pages/settings/settings.profile.vue +++ b/src/client/app/mobile/views/pages/settings/settings.profile.vue @@ -1,50 +1,55 @@ <template> - <md-card class="md-layout-item md-size-50 md-small-size-100"> + <md-card> <md-card-header> - <div class="md-title">%i18n:@title%</div> + <div class="md-title">%fa:pencil-alt% %i18n:@title%</div> </md-card-header> <md-card-content> <md-field> <label>%i18n:@name%</label> - <md-input v-model="name" :disabled="saving"/> + <md-input v-model="name" :disabled="saving" md-counter="30"/> </md-field> <md-field> + <label>%i18n:@account%</label> + <span class="md-prefix">@</span> + <md-input v-model="username" readonly></md-input> + <span class="md-suffix">@{{ host }}</span> + </md-field> + + <md-field> + <md-icon>%fa:map-marker-alt%</md-icon> <label>%i18n:@location%</label> <md-input v-model="location" :disabled="saving"/> </md-field> <md-field> - <label>%i18n:@description%</label> - <md-textarea v-model="description" :disabled="saving"/> - </md-field> - - <md-field> + <md-icon>%fa:birthday-cake%</md-icon> <label>%i18n:@birthday%</label> <md-input type="date" v-model="birthday" :disabled="saving"/> </md-field> - <div> - <div class="md-body-2">%i18n:@avatar%</div> - <md-menu md-direction="bottom-end" :md-close-on-select="true"> - <md-button md-menu-trigger>%i18n:@set-avatar%</md-button> - <md-menu-content> - <md-menu-item @click="uploadAvatar">%i18n:@upload-avatar%</md-menu-item> - <md-menu-item @click="chooseAvatar">%i18n:@choose-avatar%</md-menu-item> - </md-menu-content> - </md-menu> - </div> + <md-field> + <label>%i18n:@description%</label> + <md-textarea v-model="description" :disabled="saving" md-counter="500"/> + </md-field> + + <md-field> + <label>%i18n:@avatar%</label> + <md-file @md-change="onAvatarChange"/> + </md-field> + + <md-field> + <label>%i18n:@banner%</label> + <md-file @md-change="onBannerChange"/> + </md-field> + + <md-dialog-alert + :md-active.sync="uploading" + md-content="%18n:!@uploading%"/> <div> - <div class="md-body-2">%i18n:@banner%</div> - <md-menu md-direction="bottom-end" :md-close-on-select="true"> - <md-button md-menu-trigger>%i18n:@set-banner%</md-button> - <md-menu-content> - <md-menu-item @click="uploadAvatar">%i18n:@upload-banner%</md-menu-item> - <md-menu-item @click="chooseAvatar">%i18n:@choose-banner%</md-menu-item> - </md-menu-content> - </md-menu> + <md-switch v-model="isCat">%i18n:@is-cat%</md-switch> </div> </md-card-content> @@ -56,58 +61,83 @@ <script lang="ts"> import Vue from 'vue'; +import { apiUrl, host } from '../../../../config'; export default Vue.extend({ data() { return { + host, name: null, + username: null, location: null, description: null, birthday: null, - saving: false + avatarId: null, + bannerId: null, + isBot: false, + isCat: false, + saving: false, + uploading: false }; }, + created() { this.name = (this as any).os.i.name || ''; + this.username = (this as any).os.i.username; this.location = (this as any).os.i.profile.location; this.description = (this as any).os.i.description; this.birthday = (this as any).os.i.profile.birthday; + this.avatarId = (this as any).os.i.avatarId; + this.bannerId = (this as any).os.i.bannerId; + this.isBot = (this as any).os.i.isBot; + this.isCat = (this as any).os.i.isCat; }, + methods: { - chooseAvatar() { - (this as any).apis.chooseDriveFile({ - multiple: false - }).then(file => { - this.avatarSaving = true; + onAvatarChange([file]) { + this.uploading = true; - (this as any).api('i/update', { - avatarId: file.id - }).then(() => { - this.avatarSaving = false; - alert('%i18n:!@avatar-saved%'); - }); + const data = new FormData(); + data.append('file', file); + data.append('i', (this as any).os.i.token); + + fetch(apiUrl + '/drive/files/create', { + method: 'POST', + body: data + }) + .then(response => response.json()) + .then(f => { + this.avatarId = f.id; + this.uploading = false; + }) + .catch(e => { + this.uploading = false; + alert('%18n:!@upload-failed%'); }); }, - chooseBanner() { - (this as any).apis.chooseDriveFile({ - multiple: false - }).then(file => { - this.bannerSaving = true; - (this as any).api('i/update', { - bannerId: file.id - }).then(() => { - this.bannerSaving = false; - alert('%i18n:!@banner-saved%'); - }); + onBannerChange([file]) { + this.uploading = true; + + const data = new FormData(); + data.append('file', file); + data.append('i', (this as any).os.i.token); + + fetch(apiUrl + '/drive/files/create', { + method: 'POST', + body: data + }) + .then(response => response.json()) + .then(f => { + this.bannerId = f.id; + this.uploading = false; + }) + .catch(e => { + this.uploading = false; + alert('%18n:!@upload-failed%'); }); }, - uploadAvatar() { - // a - }, - uploadBanner() { - // a - }, + save() { this.saving = true; @@ -115,10 +145,19 @@ export default Vue.extend({ name: this.name || null, location: this.location || null, description: this.description || null, - birthday: this.birthday || null - }).then(() => { + birthday: this.birthday || null, + avatarId: this.avatarId, + bannerId: this.bannerId, + isBot: this.isBot, + isCat: this.isCat + }).then(i => { this.saving = false; - alert('%i18n:!@saved%'); + (this as any).os.i.avatarId = i.avatarId; + (this as any).os.i.avatarUrl = i.avatarUrl; + (this as any).os.i.bannerId = i.bannerId; + (this as any).os.i.bannerUrl = i.bannerUrl; + + alert('%i18n:@saved%'); }); } } diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index 34adeb03cd..84fd7eda02 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -1,7 +1,7 @@ <template> <mk-ui> <template slot="header" v-if="!fetching"><img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">{{ user | userName }}</template> - <main v-if="!fetching" :data-darkmode="_darkmode_"> + <main v-if="!fetching" :data-darkmode="$store.state.device.darkmode"> <div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div> <div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div> <header> diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue index f0a0877862..03abcabe8f 100644 --- a/src/client/app/mobile/views/pages/widgets.vue +++ b/src/client/app/mobile/views/pages/widgets.vue @@ -65,7 +65,7 @@ export default Vue.extend({ computed: { widgets(): any[] { - return this.$store.state.settings.data.mobileHome; + return this.$store.state.settings.mobileHome; } }, diff --git a/src/client/app/reset.styl b/src/client/app/reset.styl index 10bd3113a2..c0a88f27b0 100644 --- a/src/client/app/reset.styl +++ b/src/client/app/reset.styl @@ -1,3 +1,6 @@ +input + min-width 0px + input:not([type]) input[type='text'] input[type='password'] diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 1f1189054d..e300d31d8d 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -1,4 +1,6 @@ import Vuex from 'vuex'; +import createPersistedState from 'vuex-persistedstate'; + import MiOS from './mios'; const defaultSettings = { @@ -11,17 +13,36 @@ const defaultSettings = { gradientWindowHeader: false, showReplyTarget: true, showMyRenotes: true, - showRenotedMyNotes: true + showRenotedMyNotes: true, + loadRemoteMedia: true, + disableViaMobile: false +}; + +const defaultDeviceSettings = { + apiViaStream: true, + autoPopout: false, + darkmode: false, + enableSounds: true, + soundVolume: 0.5, + lang: null, + preventUpdate: false, + debug: false, + lightmode: false, + loadRawImages: false, + postStyle: 'standard' }; export default (os: MiOS) => new Vuex.Store({ plugins: [store => { store.subscribe((mutation, state) => { if (mutation.type.startsWith('settings/')) { - localStorage.setItem('settings', JSON.stringify(state.settings.data)); + localStorage.setItem('settings', JSON.stringify(state.settings)); } }); - }], + }, createPersistedState({ + paths: ['device'], + filter: mut => mut.type.startsWith('device/') + })], state: { indicate: false, @@ -39,50 +60,60 @@ export default (os: MiOS) => new Vuex.Store({ }, modules: { - settings: { + device: { namespaced: true, - state: { - data: defaultSettings - }, + state: defaultDeviceSettings, mutations: { set(state, x: { key: string; value: any }) { - state.data[x.key] = x.value; + state[x.key] = x.value; + } + } + }, + + settings: { + namespaced: true, + + state: defaultSettings, + + mutations: { + set(state, x: { key: string; value: any }) { + state[x.key] = x.value; }, setHome(state, data) { - state.data.home = data; + state.home = data; }, setHomeWidget(state, x) { - const w = state.data.home.find(w => w.id == x.id); + const w = state.home.find(w => w.id == x.id); if (w) { w.data = x.data; } }, addHomeWidget(state, widget) { - state.data.home.unshift(widget); + state.home.unshift(widget); }, setMobileHome(state, data) { - state.data.mobileHome = data; + state.mobileHome = data; }, setMobileHomeWidget(state, x) { - const w = state.data.mobileHome.find(w => w.id == x.id); + const w = state.mobileHome.find(w => w.id == x.id); if (w) { w.data = x.data; } }, addMobileHomeWidget(state, widget) { - state.data.mobileHome.unshift(widget); + state.mobileHome.unshift(widget); }, removeMobileHomeWidget(state, widget) { - state.data.mobileHome = state.data.mobileHome.filter(w => w.id != widget.id); + state.mobileHome = state.mobileHome.filter(w => w.id != widget.id); } }, @@ -108,7 +139,7 @@ export default (os: MiOS) => new Vuex.Store({ ctx.commit('addHomeWidget', widget); os.api('i/update_home', { - home: ctx.state.data.home + home: ctx.state.home }); }, @@ -116,7 +147,7 @@ export default (os: MiOS) => new Vuex.Store({ ctx.commit('addMobileHomeWidget', widget); os.api('i/update_mobile_home', { - home: ctx.state.data.mobileHome + home: ctx.state.mobileHome }); }, @@ -124,7 +155,7 @@ export default (os: MiOS) => new Vuex.Store({ ctx.commit('removeMobileHomeWidget', widget); os.api('i/update_mobile_home', { - home: ctx.state.data.mobileHome.filter(w => w.id != widget.id) + home: ctx.state.mobileHome.filter(w => w.id != widget.id) }); } } diff --git a/src/client/assets/title.svg b/src/client/assets/title.svg deleted file mode 100644 index 747fcd38b1..0000000000 --- a/src/client/assets/title.svg +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" - y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve"> -<circle fill="#2B2F2D" cx="128" cy="153.6" r="19.201"/> -<circle fill="#2B2F2D" cx="51.2" cy="153.6" r="19.2"/> -<circle fill="#2B2F2D" cx="204.8" cy="153.6" r="19.2"/> -<polyline fill="none" stroke="#2B2F2D" stroke-width="16" stroke-linejoin="round" stroke-miterlimit="10" points="51.2,153.6 - 89.601,102.4 128,153.6 166.4,102.4 204.799,153.6 "/> -<circle fill="#2B2F2D" cx="89.6" cy="102.4" r="19.2"/> -<circle fill="#2B2F2D" cx="166.4" cy="102.4" r="19.199"/> -<g> -</g> -<g> -</g> -<g> -</g> -<g> -</g> -<g> -</g> -<g> -</g> -</svg> diff --git a/src/client/assets/version.html b/src/client/assets/version.html index d8a98279a6..177d37db8f 100644 --- a/src/client/assets/version.html +++ b/src/client/assets/version.html @@ -10,11 +10,6 @@ localStorage.setItem('v', v); } - const lang = window.prompt('Enter language (optional):'); - if (lang && lang.length > 0) { - localStorage.setItem('lang', lang); - } - setTimeout(() => { location.href = '/'; }, 500); diff --git a/src/client/md.scss b/src/client/md.scss index d850863efd..8368365885 100644 --- a/src/client/md.scss +++ b/src/client/md.scss @@ -6,7 +6,7 @@ @include md-register-theme("default", ( primary: $themeColor, - accent: md-get-palette-color(red, A200) + accent: $themeColor )); @import "~vue-material/dist/components/MdButton/theme"; diff --git a/src/config/types.ts b/src/config/types.ts index dff3f7d37c..910c03c2c1 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -41,6 +41,8 @@ export type Source = { secret_key: string; }; + preventCacheRemoteFiles: boolean; + /** * ゴーストアカウントのID */ diff --git a/src/index.ts b/src/index.ts index d633fcbbcb..bcd6561691 100644 --- a/src/index.ts +++ b/src/index.ts @@ -194,7 +194,12 @@ cluster.on('exit', worker => { // Display detail of unhandled promise rejection process.on('unhandledRejection', console.dir); -// Dying away... -process.on('exit', () => { - Logger.info('The process is going exit'); +// Display detail of uncaught exception +process.on('uncaughtException', err => { + console.error(err); +}); + +// Dying away... +process.on('exit', code => { + Logger.info(`The process is going exit (${code})`); }); diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts index 8a18567dc6..a3a567038e 100644 --- a/src/models/drive-file.ts +++ b/src/models/drive-file.ts @@ -32,7 +32,7 @@ export type IMetadata = { uri?: string; url?: string; deletedAt?: Date; - isExpired?: boolean; + isMetaOnly?: boolean; }; export type IDriveFile = { @@ -155,7 +155,8 @@ export const pack = ( _target = Object.assign(_target, _file.metadata); _target.src = _file.metadata.url; - _target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`; + _target.url = _file.metadata.isMetaOnly ? _file.metadata.url : `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`; + _target.isRemote = _file.metadata.isMetaOnly; if (_target.properties == null) _target.properties = {}; diff --git a/src/models/note.ts b/src/models/note.ts index 5070923363..1274901d45 100644 --- a/src/models/note.ts +++ b/src/models/note.ts @@ -324,6 +324,10 @@ export const pack = async ( // resolve promises in _note object _note = await rap(_note); + if (_note.user.isCat && _note.text) { + _note.text = _note.text.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ'); + } + if (hide) { _note.mediaIds = []; _note.text = null; diff --git a/src/models/user.ts b/src/models/user.ts index 108111ceca..477bb232e4 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -77,6 +77,7 @@ export interface ILocalUser extends IUserBase { }; lastUsedAt: Date; isBot: boolean; + isCat: boolean; isPro: boolean; twoFactorSecret: string; twoFactorEnabled: boolean; diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts index 7b9faf4cf4..cdb87a4114 100644 --- a/src/server/api/common/read-notification.ts +++ b/src/server/api/common/read-notification.ts @@ -1,6 +1,7 @@ import * as mongo from 'mongodb'; import { default as Notification, INotification } from '../../../models/notification'; import publishUserStream from '../../../publishers/stream'; +import Mute from '../../../models/mute'; /** * Mark as read notification(s) @@ -26,6 +27,11 @@ export default ( ? [new mongo.ObjectID(message)] : [(message as INotification)._id]; + const mute = await Mute.find({ + muterId: userId + }); + const mutedUserIds = mute.map(m => m.muteeId); + // Update documents await Notification.update({ _id: { $in: ids }, @@ -42,6 +48,9 @@ export default ( const count = await Notification .count({ notifieeId: userId, + notifierId: { + $nin: mutedUserIds + }, isRead: false }, { limit: 1 diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts index 7647c76d3d..892da3540f 100644 --- a/src/server/api/endpoints.ts +++ b/src/server/api/endpoints.ts @@ -482,7 +482,7 @@ const endpoints: Endpoint[] = [ name: 'notes/replies' }, { - name: 'notes/context' + name: 'notes/conversation' }, { name: 'notes/create', diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts index e9348e4e2f..dd748d6bba 100644 --- a/src/server/api/endpoints/drive/files/create.ts +++ b/src/server/api/endpoints/drive/files/create.ts @@ -1,6 +1,7 @@ /** * Module dependencies */ +import * as fs from 'fs'; import $ from 'cafy'; import ID from '../../../../../cafy-id'; import { validateFileName, pack } from '../../../../../models/drive-file'; import create from '../../../../../services/drive/add-file'; @@ -32,15 +33,23 @@ module.exports = async (file, params, user): Promise<any> => { const [folderId = null, folderIdErr] = $.type(ID).optional().nullable().get(params.folderId); if (folderIdErr) throw 'invalid folderId param'; + function cleanup() { + fs.unlink(file.path, () => {}); + } + try { // Create file const driveFile = await create(user, file.path, name, null, folderId); + cleanup(); + // Serialize return pack(driveFile); } catch (e) { console.error(e); + cleanup(); + throw e; } }; diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts index 50ed9b27e8..ba9c47508c 100644 --- a/src/server/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -96,8 +96,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // Serialize - res(await Promise.all(notifications.map(async notification => - await pack(notification)))); + res(await Promise.all(notifications.map(notification => pack(notification)))); // Mark as read all if (notifications.length > 0 && markAsRead) { diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index b7b25d0f65..6e0c5b8515 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -47,6 +47,11 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => { if (isBotErr) return rej('invalid isBot param'); if (isBot != null) user.isBot = isBot; + // Get 'isCat' parameter + const [isCat, isCatErr] = $.bool.optional().get(params.isCat); + if (isCatErr) return rej('invalid isCat param'); + if (isCat != null) user.isCat = isCat; + // Get 'autoWatch' parameter const [autoWatch, autoWatchErr] = $.bool.optional().get(params.autoWatch); if (autoWatchErr) return rej('invalid autoWatch param'); @@ -82,6 +87,7 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => { bannerColor: user.bannerColor, profile: user.profile, isBot: user.isBot, + isCat: user.isCat, settings: user.settings } }); diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts index 4ce7613d70..21946d1bd3 100644 --- a/src/server/api/endpoints/notes.ts +++ b/src/server/api/endpoints/notes.ts @@ -8,6 +8,10 @@ import Note, { pack } from '../../../models/note'; * Get all notes */ module.exports = (params) => new Promise(async (res, rej) => { + // Get 'local' parameter + const [local, localErr] = $.bool.optional().get(params.local); + if (localErr) return rej('invalid local param'); + // Get 'reply' parameter const [reply, replyErr] = $.bool.optional().get(params.reply); if (replyErr) return rej('invalid reply param'); @@ -61,6 +65,10 @@ module.exports = (params) => new Promise(async (res, rej) => { }; } + if (local) { + query['_user.host'] = null; + } + if (reply != undefined) { query.replyId = reply ? { $exists: true, $ne: null } : null; } diff --git a/src/server/api/endpoints/notes/context.ts b/src/server/api/endpoints/notes/conversation.ts similarity index 80% rename from src/server/api/endpoints/notes/context.ts rename to src/server/api/endpoints/notes/conversation.ts index 1cd27250e2..02f7229ccf 100644 --- a/src/server/api/endpoints/notes/context.ts +++ b/src/server/api/endpoints/notes/conversation.ts @@ -5,11 +5,7 @@ import $ from 'cafy'; import ID from '../../../../cafy-id'; import Note, { pack } from '../../../../models/note'; /** - * Show a context of a note - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} + * Show conversation of a note */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'noteId' parameter @@ -33,7 +29,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { return rej('note not found'); } - const context = []; + const conversation = []; let i = 0; async function get(id) { @@ -41,10 +37,10 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const p = await Note.findOne({ _id: id }); if (i > offset) { - context.push(p); + conversation.push(p); } - if (context.length == limit) { + if (conversation.length == limit) { return; } @@ -58,6 +54,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Serialize - res(await Promise.all(context.map(async note => - await pack(note, user)))); + res(await Promise.all(conversation.map(note => pack(note, user)))); }); diff --git a/src/server/api/endpoints/notifications/get_unread_count.ts b/src/server/api/endpoints/notifications/get_unread_count.ts index 600a80d194..9766366ff1 100644 --- a/src/server/api/endpoints/notifications/get_unread_count.ts +++ b/src/server/api/endpoints/notifications/get_unread_count.ts @@ -9,8 +9,7 @@ import Mute from '../../../../models/mute'; */ module.exports = (params, user) => new Promise(async (res, rej) => { const mute = await Mute.find({ - muterId: user._id, - deletedAt: { $exists: false } + muterId: user._id }); const mutedUserIds = mute.map(m => m.muteeId); diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts index d613a3aa5f..e04400317f 100644 --- a/src/server/file/send-drive-file.ts +++ b/src/server/file/send-drive-file.ts @@ -33,11 +33,12 @@ export default async function(ctx: Koa.Context) { if (file.metadata.deletedAt) { ctx.status = 410; - if (file.metadata.isExpired) { - await send(ctx, '/cache-expired.png', { root: assets }); - } else { - await send(ctx, '/tombstone.png', { root: assets }); - } + await send(ctx, '/tombstone.png', { root: assets }); + return; + } + + if (file.metadata.isMetaOnly) { + ctx.status = 204; return; } diff --git a/src/server/index.ts b/src/server/index.ts index ded8f7706e..fc3d252e10 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -4,8 +4,7 @@ import * as fs from 'fs'; import * as http from 'http'; -import * as https from 'https'; -//import * as http2 from 'http2'; +import * as http2 from 'http2'; import * as zlib from 'zlib'; import * as Koa from 'koa'; import * as Router from 'koa-router'; @@ -68,8 +67,7 @@ function createServer() { certs[k] = fs.readFileSync(config.https[k]); }); certs['allowHTTP1'] = true; - //return http2.createSecureServer(certs, app.callback()); - return https.createServer(certs, app.callback()); + return http2.createSecureServer(certs, app.callback()); } else { return http.createServer(app.callback()); } diff --git a/src/server/web/index.ts b/src/server/web/index.ts index 6ceef17c1c..5ce040d083 100644 --- a/src/server/web/index.ts +++ b/src/server/web/index.ts @@ -49,8 +49,8 @@ const router = new Router(); //#region static assets router.get('/assets/*', async ctx => { - // 無圧縮スクリプトを用意するのは大変なので一時的に無効化 - const path = process.env.NODE_ENV == 'production' ? ctx.path.replace('raw.js', 'min.js') : ctx.path.replace('min.js', 'raw.js'); + // 互換性のため + const path = ctx.path.replace('.raw.js', '.js').replace('.min.js', '.js'); await send(ctx, path, { root: client, maxage: ms('7 days'), diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index bcd5bee512..0e42a00bf6 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -1,6 +1,5 @@ import { Buffer } from 'buffer'; import * as fs from 'fs'; -import * as tmp from 'tmp'; import * as stream from 'stream'; import * as mongodb from 'mongodb'; @@ -14,8 +13,7 @@ import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile, DriveFileChunk } import DriveFolder from '../../models/drive-folder'; import { pack } from '../../models/drive-file'; import event, { publishDriveStream } from '../../publishers/stream'; -import getAcct from '../../acct/render'; -import { IUser, isLocalUser, isRemoteUser } from '../../models/user'; +import { isLocalUser, IUser, IRemoteUser } from '../../models/user'; import DriveFileThumbnail, { getDriveFileThumbnailBucket, DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail'; import genThumbnail from '../../drive/gen-thumbnail'; @@ -25,13 +23,6 @@ const gm = _gm.subClass({ const log = debug('misskey:drive:add-file'); -const tmpFile = (): Promise<[string, any]> => new Promise((resolve, reject) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return reject(e); - resolve([path, cleanup]); - }); -}); - const writeChunks = (name: string, readable: stream.Readable, type: string, metadata: any) => getDriveFileBucket() .then(bucket => new Promise((resolve, reject) => { @@ -55,64 +46,115 @@ const writeThumbnailChunks = (name: string, readable: stream.Readable, originalI readable.pipe(writeStream); })); -const addFile = async ( +async function deleteOldFile(user: IRemoteUser) { + const oldFile = await DriveFile.findOne({ + _id: { + $nin: [user.avatarId, user.bannerId] + } + }, { + sort: { + _id: 1 + } + }); + + if (oldFile) { + // チャンクをすべて削除 + DriveFileChunk.remove({ + files_id: oldFile._id + }); + + DriveFile.update({ _id: oldFile._id }, { + $set: { + 'metadata.deletedAt': new Date(), + 'metadata.isExpired': true + } + }); + + //#region サムネイルもあれば削除 + const thumbnail = await DriveFileThumbnail.findOne({ + 'metadata.originalId': oldFile._id + }); + + if (thumbnail) { + DriveFileThumbnailChunk.remove({ + files_id: thumbnail._id + }); + + DriveFileThumbnail.remove({ _id: thumbnail._id }); + } + //#endregion + } +} + +/** + * Add file to drive + * + * @param user User who wish to add file + * @param path File path + * @param name Name + * @param comment Comment + * @param folderId Folder ID + * @param force If set to true, forcibly upload the file even if there is a file with the same hash. + * @return Created drive file + */ +export default async function( user: IUser, path: string, name: string = null, comment: string = null, folderId: mongodb.ObjectID = null, force: boolean = false, + metaOnly: boolean = false, url: string = null, uri: string = null -): Promise<IDriveFile> => { - log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`); - - // Calculate hash, get content type and get file size - const [hash, [mime, ext], size] = await Promise.all([ - // hash - ((): Promise<string> => new Promise((res, rej) => { - const readable = fs.createReadStream(path); - const hash = crypto.createHash('md5'); - const chunks = []; - readable - .on('error', rej) - .pipe(hash) - .on('error', rej) - .on('data', (chunk) => chunks.push(chunk)) - .on('end', () => { - const buffer = Buffer.concat(chunks); - res(buffer.toString('hex')); - }); - }))(), - // mime - ((): Promise<[string, string | null]> => new Promise((res, rej) => { - const readable = fs.createReadStream(path); - readable - .on('error', rej) - .once('data', (buffer: Buffer) => { - readable.destroy(); - const type = fileType(buffer); - if (type) { - return res([type.mime, type.ext]); - } else { - // 種類が同定できなかったら application/octet-stream にする - return res(['application/octet-stream', null]); - } - }); - }))(), - // size - ((): Promise<number> => new Promise((res, rej) => { - fs.stat(path, (err, stats) => { - if (err) return rej(err); - res(stats.size); +): Promise<IDriveFile> { + // Calc md5 hash + const calcHash = new Promise<string>((res, rej) => { + const readable = fs.createReadStream(path); + const hash = crypto.createHash('md5'); + const chunks = []; + readable + .on('error', rej) + .pipe(hash) + .on('error', rej) + .on('data', chunk => chunks.push(chunk)) + .on('end', () => { + const buffer = Buffer.concat(chunks); + res(buffer.toString('hex')); }); - }))() - ]); + }); + + // Detect content type + const detectMime = new Promise<[string, string]>((res, rej) => { + const readable = fs.createReadStream(path); + readable + .on('error', rej) + .once('data', (buffer: Buffer) => { + readable.destroy(); + const type = fileType(buffer); + if (type) { + res([type.mime, type.ext]); + } else { + // 種類が同定できなかったら application/octet-stream にする + res(['application/octet-stream', null]); + } + }); + }); + + // Get file size + const getFileSize = new Promise<number>((res, rej) => { + fs.stat(path, (err, stats) => { + if (err) return rej(err); + res(stats.size); + }); + }); + + const [hash, [mime, ext], size] = await Promise.all([calcHash, detectMime, getFileSize]); log(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`); // detect name - const detectedName: string = name || (ext ? `untitled.${ext}` : 'untitled'); + const detectedName = name || (ext ? `untitled.${ext}` : 'untitled'); if (!force) { // Check if there is a file with the same hash @@ -125,26 +167,72 @@ const addFile = async ( if (much !== null) { log('file with same hash is found'); return much; - } else { - log('file with same hash is not found'); } } - const [wh, averageColor, folder] = await Promise.all([ - // Width and height (when image) - (async () => { - // 画像かどうか - if (!/^image\/.*$/.test(mime)) { - return null; + //#region Check drive usage + if (!metaOnly) { + const usage = await DriveFile + .aggregate([{ + $match: { + 'metadata.userId': user._id, + 'metadata.deletedAt': { $exists: false } + } + }, { + $project: { + length: true + } + }, { + $group: { + _id: null, + usage: { $sum: '$length' } + } + }]) + .then((aggregates: any[]) => { + if (aggregates.length > 0) { + return aggregates[0].usage; + } + return 0; + }); + + log(`drive usage is ${usage}`); + + // If usage limit exceeded + if (usage + size > user.driveCapacity) { + if (isLocalUser(user)) { + throw 'no-free-space'; + } else { + // (アバターまたはバナーを含まず)最も古いファイルを削除する + deleteOldFile(user); } + } + } + //#endregion - const imageType = mime.split('/')[1]; + const fetchFolder = async () => { + if (!folderId) { + return null; + } - // 画像でもPNGかJPEGかGIFでないならスキップ - if (imageType != 'png' && imageType != 'jpeg' && imageType != 'gif') { - return null; - } + const driveFolder = await DriveFolder.findOne({ + _id: folderId, + userId: user._id + }); + if (driveFolder == null) throw 'folder-not-found'; + + return driveFolder; + }; + + const properties = {}; + + let propPromises = []; + + const isImage = ['image/jpeg', 'image/gif', 'image/png'].includes(mime); + + if (isImage) { + // Calc width and height + const calcWh = async () => { log('calculate image width and height...'); // Calculate width and height @@ -153,22 +241,12 @@ const addFile = async ( log(`image width and height is calculated: ${size.width}, ${size.height}`); - return [size.width, size.height]; - })(), - // average color (when image) - (async () => { - // 画像かどうか - if (!/^image\/.*$/.test(mime)) { - return null; - } - - const imageType = mime.split('/')[1]; - - // 画像でもPNGかJPEGでないならスキップ - if (imageType != 'png' && imageType != 'jpeg') { - return null; - } + properties['width'] = size.width; + properties['height'] = size.height; + }; + // Calc average color + const calcAvg = async () => { log('calculate average color...'); const info = await prominence(gm(fs.createReadStream(path), name)).identify(); @@ -185,111 +263,15 @@ const addFile = async ( log(`average color is calculated: ${r}, ${g}, ${b}`); - return isTransparent ? [r, g, b, 255] : [r, g, b]; - })(), - // folder - (async () => { - if (!folderId) { - return null; - } - const driveFolder = await DriveFolder.findOne({ - _id: folderId, - userId: user._id - }); - if (!driveFolder) { - throw 'folder-not-found'; - } - return driveFolder; - })(), - // usage checker - (async () => { - // Calculate drive usage - const usage = await DriveFile - .aggregate([{ - $match: { - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then((aggregates: any[]) => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); + const value = isTransparent ? [r, g, b, 255] : [r, g, b]; - log(`drive usage is ${usage}`); + properties['avgColor'] = value; + }; - // If usage limit exceeded - if (usage + size > user.driveCapacity) { - if (isLocalUser(user)) { - throw 'no-free-space'; - } else { - //#region (アバターまたはバナーを含まず)最も古いファイルを削除する - const oldFile = await DriveFile.findOne({ - _id: { - $nin: [user.avatarId, user.bannerId] - } - }, { - sort: { - _id: 1 - } - }); - - if (oldFile) { - // チャンクをすべて削除 - DriveFileChunk.remove({ - files_id: oldFile._id - }); - - DriveFile.update({ _id: oldFile._id }, { - $set: { - 'metadata.deletedAt': new Date(), - 'metadata.isExpired': true - } - }); - - //#region サムネイルもあれば削除 - const thumbnail = await DriveFileThumbnail.findOne({ - 'metadata.originalId': oldFile._id - }); - - if (thumbnail) { - DriveFileThumbnailChunk.remove({ - files_id: thumbnail._id - }); - - DriveFileThumbnail.remove({ _id: thumbnail._id }); - } - //#endregion - } - //#endregion - } - } - })() - ]); - - const readable = fs.createReadStream(path); - - const properties = {}; - - if (wh) { - properties['width'] = wh[0]; - properties['height'] = wh[1]; + propPromises = [calcWh(), calcAvg()]; } - if (averageColor) { - properties['avgColor'] = averageColor; - } + const [folder] = await Promise.all([fetchFolder(), propPromises]); const metadata = { userId: user._id, @@ -298,7 +280,8 @@ const addFile = async ( }, folderId: folder !== null ? folder._id : null, comment: comment, - properties: properties + properties: properties, + isMetaOnly: metaOnly } as IMetadata; if (url !== null) { @@ -309,74 +292,35 @@ const addFile = async ( metadata.uri = uri; } - const file = await (writeChunks(detectedName, readable, mime, metadata) as Promise<IDriveFile>); + const driveFile = metaOnly + ? await DriveFile.insert({ + length: 0, + uploadDate: new Date(), + md5: hash, + filename: detectedName, + metadata: metadata, + contentType: mime + }) + : await (writeChunks(detectedName, fs.createReadStream(path), mime, metadata) as Promise<IDriveFile>); - try { - const thumb = await genThumbnail(file); - if (thumb) { - await writeThumbnailChunks(detectedName, thumb, file._id); + log(`drive file has been created ${driveFile._id}`); + + pack(driveFile).then(packedFile => { + // Publish drive_file_created event + event(user._id, 'drive_file_created', packedFile); + publishDriveStream(user._id, 'file_created', packedFile); + }); + + if (!metaOnly) { + try { + const thumb = await genThumbnail(driveFile); + if (thumb) { + await writeThumbnailChunks(detectedName, thumb, driveFile._id); + } + } catch (e) { + // noop } - } catch (e) { - // noop } - return file; -}; - -/** - * Add file to drive - * - * @param user User who wish to add file - * @param file File path or readableStream - * @param comment Comment - * @param type File type - * @param folderId Folder ID - * @param force If set to true, forcibly upload the file even if there is a file with the same hash. - * @return Object that represents added file - */ -export default (user: any, file: string | stream.Readable, ...args) => new Promise<any>((resolve, reject) => { - const isStream = typeof file === 'object' && typeof file.read === 'function'; - - // Get file path - new Promise<[string, any]>((res, rej) => { - if (typeof file === 'string') { - res([file, null]); - } else if (isStream) { - tmpFile() - .then(([path, cleanup]) => { - const readable: stream.Readable = file; - const writable = fs.createWriteStream(path); - readable - .on('error', rej) - .on('end', () => { - res([path, cleanup]); - }) - .pipe(writable) - .on('error', rej); - }) - .catch(rej); - } else { - rej(new Error('un-compatible file.')); - } - }) - .then(([path, cleanup]) => new Promise<IDriveFile>((res, rej) => { - addFile(user, path, ...args) - .then(file => { - res(file); - if (cleanup) cleanup(); - }) - .catch(rej); - })) - .then(file => { - log(`drive file has been created ${file._id}`); - - resolve(file); - - pack(file).then(packedFile => { - // Publish drive_file_created event - event(user._id, 'drive_file_created', packedFile); - publishDriveStream(user._id, 'file_created', packedFile); - }); - }) - .catch(reject); -}); + return driveFile; +} diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts index ad2620c036..e216ca603d 100644 --- a/src/services/drive/upload-from-url.ts +++ b/src/services/drive/upload-from-url.ts @@ -1,14 +1,17 @@ +import * as fs from 'fs'; import * as URL from 'url'; -import { IDriveFile, validateFileName } from '../../models/drive-file'; -import create from './add-file'; + import * as debug from 'debug'; import * as tmp from 'tmp'; -import * as fs from 'fs'; import * as request from 'request'; +import { IDriveFile, validateFileName } from '../../models/drive-file'; +import create from './add-file'; +import config from '../../config'; + const log = debug('misskey:drive:upload-from-url'); -export default async (url, user, folderId = null, uri = null): Promise<IDriveFile> => { +export default async (url: string, user, folderId = null, uri: string = null): Promise<IDriveFile> => { log(`REQUESTED: ${url}`); let name = URL.parse(url).pathname.split('/').pop(); @@ -43,7 +46,7 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil let error; try { - driveFile = await create(user, path, name, null, folderId, false, url, uri); + driveFile = await create(user, path, name, null, folderId, false, config.preventCacheRemoteFiles, url, uri); log(`created: ${driveFile._id}`); } catch (e) { error = e; diff --git a/src/services/note/create.ts b/src/services/note/create.ts index f049c34b65..b9ff1f679b 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -283,6 +283,8 @@ export default async (user: IUser, data: { mentionedUsers = mentionedUsers.filter(x => x != null); mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => { + event(u, 'mention', noteObj); + // 既に言及されたユーザーに対する返信や引用renoteの場合も無視 if (data.reply && data.reply.userId.equals(u._id)) return; if (data.renote && data.renote.userId.equals(u._id)) return; diff --git a/webpack.config.ts b/webpack.config.ts index 3aeecbd8a7..67fb929449 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -60,7 +60,7 @@ const entry = { const output = { path: __dirname + '/built/client/assets', - filename: `[name].${version}.-.${isProduction ? 'min' : 'raw'}.js` + filename: `[name].${version}.-.js` }; //#region Define consts @@ -78,6 +78,7 @@ const consts = { _WS_URL_: config.ws_url, _DEV_URL_: config.dev_url, _LANG_: '%lang%', + _LANGS_: Object.keys(locales).map(l => [l, locales[l].meta.lang]), _HOST_: config.host, _HOSTNAME_: config.hostname, _URL_: config.url, @@ -110,14 +111,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`, 'utf-8'); + let src = fs.readFileSync(`${__dirname}/built/client/assets/${file}.${version}.-.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, 'utf-8'); + fs.writeFileSync(`${__dirname}/built/client/assets/${file}.${version}.${lang}.js`, src, 'utf-8'); }); }); //#endregion @@ -232,15 +233,14 @@ module.exports = { }, { loader: 'replace', query: { - search: i18nPattern.toString(), - replace: 'i18nReplacement', - i18n: true - } - }, { - loader: 'replace', - query: { - search: faPattern.toString(), - replace: 'faReplacement' + qs: [{ + search: i18nPattern.toString(), + replace: 'i18nReplacement', + i18n: true + }, { + search: faPattern.toString(), + replace: 'faReplacement' + }] } }] }] diff --git a/webpack/i18n.ts b/webpack/i18n.ts index de4d02e9d9..e2cce060e8 100644 --- a/webpack/i18n.ts +++ b/webpack/i18n.ts @@ -2,17 +2,12 @@ * Replace i18n texts */ -export const pattern = /%i18n:([a-z0-9_\-@\.\!]+?)%/g; +export const pattern = /%i18n:([a-z0-9_\-@\.]+?)%/g; export const replacement = (ctx, match, key) => { const client = '/src/client/app/'; let name = null; - const shouldEscape = key[0] == '!'; - if (shouldEscape) { - key = key.substr(1); - } - if (key[0] == '@') { name = ctx.src.substr(ctx.src.indexOf(client) + client.length); key = key.substr(1); @@ -20,5 +15,5 @@ export const replacement = (ctx, match, key) => { const path = name ? `${name}|${key}` : key; - return shouldEscape ? `%i18n:!${path}%` : `%i18n:${path}%`; + return `%i18n:${path}%`; };