diff --git a/README.md b/README.md index f8738421c..7a10f5b81 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,7 @@ [![][travis-badge]][travis-link] [![][dependencies-badge]][dependencies-link] -[![][himawari-badge]][himasaku] -[![][sakurako-badge]][himasaku] -[](http://makeapullrequest.com) +[](http://makeapullrequest.com) [](https://greenkeeper.io/) > Lead Maintainer: [syuilo][syuilo-link] @@ -16,7 +14,7 @@ ultimately sophisticated professional microblogging software. <a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a> - + :sparkles: Features ---------------------------------------------------------------- @@ -26,7 +24,7 @@ ultimately sophisticated professional microblogging software. * and widgets! * Private messages * Mute -* Streaming +* Real-time timelines * ActivityPub compatible and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz). @@ -49,9 +47,9 @@ If you want to... [![Backers][backers-image]][support-url] [![Sponsors][sponsors-image]][support-url] -| ![][ooo-icon] | -|:-:| -| [ooo][ooo-link] | +| ![][nagarus-icon] | ![][dansup-icon] | +|:-:|:-:| +| [nagarus][nagarus-link] | [dansup][dansup-link] | :four_leaf_clover: Copyright ---------------------------------------------------------------- @@ -67,9 +65,6 @@ Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE). [travis-badge]: http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square [dependencies-link]: https://david-dm.org/syuilo/misskey [dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square -[himasaku]: https://himasaku.net -[himawari-badge]: https://img.shields.io/badge/%E5%8F%A4%E8%B0%B7-%E5%90%91%E6%97%A5%E8%91%B5-1684c5.svg?style=flat-square -[sakurako-badge]: https://img.shields.io/badge/%E5%A4%A7%E5%AE%A4-%E6%AB%BB%E5%AD%90-efb02a.svg?style=flat-square [backer-url]: #backers [backer-badge]: https://opencollective.com/misskey/backers/badge.svg @@ -82,5 +77,8 @@ Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE). [syuilo-link]: https://syuilo.com [syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70 -[ooo-link]: https://www.patreon.com/user/creators?u=11601413 -[ooo-icon]: https://c10.patreonusercontent.com/3/eyJ2IjoiMSIsInciOjIwMH0%3D/patreon-media/user/11601413/20cb15f209924302b399b99d3c98b850?token-time=2145916800&token-hash=IO31nK6VZCMWBWU2VAk2c824BX2QZ4DNPKyHHZXS0iw%3D +[nagarus-link]: https://www.patreon.com/user/creators?u=11601413 +[nagarus-icon]: https://c10.patreonusercontent.com/3/eyJ2IjoiMSIsInciOjIwMH0%3D/patreon-media/user/11601413/20cb15f209924302b399b99d3c98b850?token-time=2145916800&token-hash=IO31nK6VZCMWBWU2VAk2c824BX2QZ4DNPKyHHZXS0iw%3D +[dansup-link]: https://www.patreon.com/dansup +[dansup-icon]: https://c10.patreonusercontent.com/3/eyJ2IjoiMSIsInciOjIwMH0%3D/patreon-media/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb?token-time=2145916800&token-hash=opXAM_pnhUTuN1jCA6p_Nn_YsaqohY465YFjWFqMEEE%3D + diff --git a/DONATORS.md b/docs/DONATORS.md similarity index 74% rename from DONATORS.md rename to docs/DONATORS.md index 6fe5df04b..9da5c1a94 100644 --- a/DONATORS.md +++ b/docs/DONATORS.md @@ -22,8 +22,4 @@ The list of people who have sent donation for Misskey. --- -If your name is missing, please contact us! - -If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link]. - -[syuilo-link]: https://syuilo.com +If your name is missing, please contact us! diff --git a/docs/donate.ja.md b/docs/donate.ja.md deleted file mode 100644 index b19d7bc37..000000000 --- a/docs/donate.ja.md +++ /dev/null @@ -1,26 +0,0 @@ -# Misskeyにカンパする方法 -Misskeyのサポートにご興味をお持ちいただきありがとうございます! -Misskeyにカンパをしていただくと、貴方のお名前と好きなURLなどをMisskeyのリポジトリに刻む権利がもらえます。 - -Misskeyにカンパして開発・運営をサポートするには、次のいくつかの方法があります: - -## ConoHaカードを購入する -(本家)Misskeyは、ConoHaというVPSサービスを利用しています。ConoHaカードを購入して、 -カードに記載されているクーポンコードを syuilotan@yahoo.co.jp までお送りいただければ、 -そのクーポンをチャージしてサーバーの運営費に充てることができます。 - -ConoHaカードについてはこちらをご覧ください: https://www.conoha.jp/conohacard/ - -Amazonでも買えます: https://www.amazon.co.jp/dp/B01N9E3416 - -## Amazonギフトカード -これは間接的な方法です。 - -## 銀行振込 -syuilotan@yahoo.co.jp までお問い合わせください。 - -## 手渡し -オフ会を行ったときなどに行使できる方法です。 - -## その他 -なにかいいアイデアがあればお教えください。 diff --git a/locales/en.yml b/locales/en.yml index 3b80c9b58..cdb179fe9 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -553,7 +553,7 @@ desktop/views/components/timeline.vue: global: "Global" list: "Lists" desktop/views/components/ui.header.vue: - welcome-back: "おかえりなさい、" + welcome-back: "Welcome back, " desktop/views/components/ui.header.account.vue: profile: "Your profile" drive: "Drive" diff --git a/locales/index.ts b/locales/index.ts index 2ae84f30a..45b5df095 100644 --- a/locales/index.ts +++ b/locales/index.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import * as yaml from 'js-yaml'; -export type LangKey = 'de' | 'en' | 'fr' | 'ja' | 'pl'; +export type LangKey = 'de' | 'en' | 'fr' | 'ja' | 'pl' | 'es'; export type LocaleObject = { [key: string]: any }; const loadLang = (lang: LangKey) => yaml.safeLoad( @@ -18,7 +18,8 @@ const langs: { [key: string]: LocaleObject } = { 'en': loadLang('en'), 'fr': loadLang('fr'), 'ja': native, - 'pl': loadLang('pl') + 'pl': loadLang('pl'), + 'es': loadLang('es') }; Object.entries(langs).map(([, locale]) => { diff --git a/locales/ja.yml b/locales/ja.yml index f2a85eb4e..fc790d8c5 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -47,12 +47,22 @@ common: e: "ここに書いてください" f: "あなたが書くのを待っています..." + search: "検索" delete: "削除" loading: "読み込み中" ok: "わかった" + update-available-title: "更新があります" update-available: "Misskeyの新しいバージョンがあります({newer}。現在{current}を利用中)。ページを再度読み込みすると更新が適用されます。" my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。" i-like-sushi: "私は(プリンよりむしろ)寿司が好き" + show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示" + + reversi: + drawn: "引き分け" + my-turn: "あなたのターンです" + opponent-turn: "相手のターンです" + turn-of: "{}のターンです" + past-turn-of: "{}のターン" widgets: analog-clock: "アナログ時計" @@ -270,6 +280,13 @@ common/views/widgets/memo.vue: memo: "ここに書いて!" save: "保存" +common/views/pages/follow.vue: + signed-in-as: "{}としてサインイン中" + following: "フォロー中" + follow: "フォロー" + request-pending: "フォロー許可待ち" + follow-request: "フォロー申請" + desktop/views/components/activity.chart.vue: total: "Black ... Total" notes: "Blue ... Notes" @@ -444,7 +461,7 @@ desktop/views/components/post-form.vue: attach-media-from-local: "PCからメディアを添付" attach-media-from-drive: "ドライブからメディアを添付" attach-cancel: "添付取り消し" - insert-a-kao: "v(‘ω’)v" + insert-a-kao: "v('ω')v" create-poll: "アンケートを作成" text-remain: "残り{}文字" @@ -622,6 +639,9 @@ desktop/views/components/timeline.vue: global: "グローバル" list: "リスト" +desktop/views/components/ui.header.vue: + welcome-back: "おかえりなさい、" + desktop/views/components/ui.header.account.vue: profile: "プロフィール" drive: "ドライブ" @@ -727,7 +747,7 @@ desktop/views/pages/user/user.friends.vue: loading: "読み込み中" no-users: "よく話すユーザーはいません" -desktop/views/pages/user/user.header.vue: +desktop/views/pages/user/user.vue: is-suspended: "このユーザーは凍結されています。" is-remote: "このユーザーはリモートユーザーです。" view-remote: "正確な情報を見る" @@ -749,6 +769,12 @@ desktop/views/pages/user/user.profile.vue: muted: "ミュートしています" unmute: "ミュート解除" +desktop/views/pages/user/user.header.vue: + posts: "投稿" + following: "フォロー" + followers: "フォロワー" + is-bot: "このアカウントはBotです" + desktop/views/pages/user/user.timeline.vue: default: "投稿" with-replies: "投稿と返信" diff --git a/package.json b/package.json index 9961782c9..7c84161a9 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "misskey", "author": "syuilo <i@syuilo.com>", - "version": "4.3.0", - "clientVersion": "1.0.6630", + "version": "4.14.0", + "clientVersion": "1.0.6815", "codename": "nighthike", "main": "./built/index.js", "private": true, @@ -33,7 +33,8 @@ "@types/bcryptjs": "2.4.1", "@types/debug": "0.0.30", "@types/deep-equal": "1.0.1", - "@types/elasticsearch": "5.0.23", + "@types/elasticsearch": "5.0.24", + "@types/file-type": "5.2.1", "@types/gm": "1.18.0", "@types/gulp": "3.8.36", "@types/gulp-htmlmin": "1.3.32", @@ -42,51 +43,52 @@ "@types/gulp-replace": "0.0.31", "@types/gulp-uglify": "3.0.5", "@types/gulp-util": "3.0.34", - "@types/inquirer": "0.0.41", + "@types/inquirer": "0.0.42", "@types/is-root": "1.0.0", "@types/is-url": "1.2.28", "@types/js-yaml": "3.11.1", - "@types/koa": "2.0.45", - "@types/koa-bodyparser": "4.2.0", + "@types/jsdom": "11.0.6", + "@types/koa": "2.0.46", + "@types/koa-bodyparser": "5.0.0", "@types/koa-compress": "2.0.8", "@types/koa-favicon": "2.0.19", "@types/koa-logger": "3.1.0", "@types/koa-mount": "3.0.1", "@types/koa-multer": "1.0.0", - "@types/koa-router": "7.0.28", + "@types/koa-router": "7.0.30", "@types/koa-send": "4.1.1", "@types/koa-views": "2.0.3", "@types/koa__cors": "2.2.2", - "@types/kue": "0.11.8", + "@types/kue": "0.11.9", "@types/license-checker": "15.0.0", "@types/mkdirp": "0.5.2", - "@types/mocha": "5.2.0", - "@types/mongodb": "3.0.18", + "@types/mocha": "5.2.3", + "@types/mongodb": "3.0.21", "@types/ms": "0.7.30", - "@types/node": "10.1.2", + "@types/node": "10.3.6", "@types/nopt": "3.0.29", - "@types/parse5": "3.0.0", + "@types/parse5": "5.0.0", "@types/pug": "2.0.4", - "@types/qrcode": "0.8.1", + "@types/qrcode": "1.2.0", "@types/ratelimiter": "2.1.28", "@types/redis": "2.8.6", - "@types/request": "2.47.0", - "@types/request-promise-native": "1.0.14", + "@types/request": "2.47.1", + "@types/request-promise-native": "1.0.15", "@types/rimraf": "2.0.2", "@types/seedrandom": "2.4.27", "@types/single-line-log": "1.1.0", "@types/speakeasy": "2.0.2", "@types/tmp": "0.0.33", "@types/uuid": "3.4.3", - "@types/webpack": "4.4.0", + "@types/webpack": "4.4.3", "@types/webpack-stream": "3.2.10", "@types/websocket": "0.0.39", - "@types/ws": "5.1.1", + "@types/ws": "5.1.2", "animejs": "2.2.0", "autosize": "4.0.2", "autwh": "0.1.0", "bcryptjs": "2.4.3", - "bootstrap-vue": "2.0.0-rc.6", + "bootstrap-vue": "2.0.0-rc.11", "cafy": "8.0.0", "chalk": "2.4.1", "crc-32": "1.2.0", @@ -95,12 +97,12 @@ "deep-equal": "1.0.1", "deepcopy": "0.6.3", "diskusage": "0.2.4", - "dompurify": "1.0.4", + "dompurify": "1.0.5", "elasticsearch": "15.0.0", - "element-ui": "2.3.9", + "element-ui": "2.4.1", "emojilib": "2.2.12", "escape-regexp": "0.0.1", - "eslint": "4.19.1", + "eslint": "5.0.1", "eslint-plugin-vue": "4.5.0", "eventemitter3": "3.1.0", "exif-js": "2.3.0", @@ -114,7 +116,7 @@ "gulp-imagemin": "4.1.0", "gulp-mocha": "6.0.0", "gulp-pug": "4.0.1", - "gulp-rename": "1.2.3", + "gulp-rename": "1.3.0", "gulp-replace": "1.0.0", "gulp-sourcemaps": "2.6.4", "gulp-stylus": "2.7.0", @@ -122,14 +124,14 @@ "gulp-typescript": "4.0.2", "gulp-uglify": "3.0.0", "gulp-util": "3.0.8", - "hard-source-webpack-plugin": "0.6.10", + "hard-source-webpack-plugin": "0.9.0", "highlight.js": "9.12.0", - "html-minifier": "3.5.16", + "html-minifier": "3.5.17", "http-signature": "1.2.0", - "inquirer": "5.2.0", + "inquirer": "6.0.0", "is-root": "2.0.0", "is-url": "1.2.4", - "js-yaml": "3.11.0", + "js-yaml": "3.12.0", "jsdom": "11.11.0", "koa": "2.5.1", "koa-bodyparser": "4.2.1", @@ -140,11 +142,11 @@ "koa-mount": "3.0.0", "koa-multer": "1.0.2", "koa-router": "7.4.0", - "koa-send": "4.1.3", + "koa-send": "5.0.0", "koa-slow": "2.1.0", "koa-views": "6.1.4", "kue": "0.11.6", - "license-checker": "20.0.0", + "license-checker": "20.1.0", "loader-utils": "1.1.0", "mecab-async": "0.1.2", "mkdirp": "0.5.1", @@ -155,7 +157,7 @@ "ms": "2.1.1", "nan": "2.10.0", "node-sass": "4.9.0", - "node-sass-json-importer": "3.2.0", + "node-sass-json-importer": "3.3.1", "nopt": "4.0.1", "nprogress": "0.2.0", "object-assign-deep": "0.4.0", @@ -168,7 +170,7 @@ "pug": "2.0.3", "punycode": "2.1.1", "qrcode": "1.2.0", - "ratelimiter": "3.0.3", + "ratelimiter": "3.1.0", "recaptcha-promise": "0.1.3", "reconnecting-websocket": "3.2.2", "redis": "2.8.0", @@ -177,7 +179,7 @@ "rimraf": "2.6.2", "rndstr": "1.0.0", "s-age": "1.1.2", - "sass-loader": "7.0.1", + "sass-loader": "7.0.3", "seedrandom": "2.4.3", "single-line-log": "1.1.2", "speakeasy": "2.0.0", @@ -190,33 +192,38 @@ "tcp-port-used": "0.1.2", "textarea-caret": "3.1.0", "tmp": "0.0.33", - "ts-loader": "4.3.0", - "ts-node": "6.0.4", + "ts-loader": "4.4.1", + "ts-node": "7.0.0", "tslint": "5.10.0", - "typescript": "2.8.3", - "typescript-eslint-parser": "15.0.0", + "typescript": "2.9.2", + "typescript-eslint-parser": "16.0.0", "uglify-es": "3.3.9", "url-loader": "1.0.1", - "uuid": "3.2.1", + "uuid": "3.3.0", "v-animate-css": "0.0.2", "vue": "2.5.16", - "vue-cropperjs": "2.2.0", - "vue-js-modal": "1.3.13", + "vue-cropperjs": "2.2.1", + "vue-js-modal": "1.3.15", "vue-json-tree-view": "2.1.4", - "vue-loader": "15.2.1", + "vue-loader": "15.2.4", "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", + "web-push": "3.3.2", "webfinger.js": "2.6.6", - "webpack": "4.9.1", - "webpack-cli": "2.1.4", + "webpack": "4.12.1", + "webpack-cli": "3.0.8", "websocket": "1.0.26", - "ws": "5.2.0", - "xev": "2.0.1", - "@types/file-type": "5.2.1", - "@types/jsdom": "11.0.5" + "ws": "5.2.1", + "xev": "2.0.1" + }, + "greenkeeper": { + "ignore": [ + "deepcopy", + "cafy", + "@types/gulp" + ] } } diff --git a/src/client/app/common/scripts/can-hide-text.ts b/src/client/app/common/scripts/can-hide-text.ts deleted file mode 100644 index 4a4be8d9d..000000000 --- a/src/client/app/common/scripts/can-hide-text.ts +++ /dev/null @@ -1,16 +0,0 @@ -export default function(note) { - if (note.text == null) return true; - - let txt = note.text; - - if (note.media) { - note.media.forEach(file => { - txt = txt.replace(file.url, ''); - if (file.src) txt = txt.replace(file.src, ''); - }); - - if (txt == '') return true; - } - - return false; -} diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts index b5ba6916d..4445eefc3 100644 --- a/src/client/app/common/scripts/check-for-update.ts +++ b/src/client/app/common/scripts/check-for-update.ts @@ -23,7 +23,10 @@ export default async function(mios: MiOS, force = false, silent = false) { } if (!silent) { - alert('%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current)); + mios.apis.dialog({ + title: '%i18n:common.update-available-title%', + text: '%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current) + }); } return newer; diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts index cc28f7599..2e58649ac 100644 --- a/src/client/app/common/scripts/compose-notification.ts +++ b/src/client/app/common/scripts/compose-notification.ts @@ -20,34 +20,6 @@ export default function(type, data): Notification { icon: data.url + '?thumbnail&size=64' }; - case 'mention': - return { - title: `${getUserName(data.user)}さんから:`, - body: getNoteSummary(data), - icon: data.user.avatarUrl + '?thumbnail&size=64' - }; - - case 'reply': - return { - title: `${getUserName(data.user)}さんから返信:`, - body: getNoteSummary(data), - icon: data.user.avatarUrl + '?thumbnail&size=64' - }; - - case 'quote': - return { - title: `${getUserName(data.user)}さんが引用:`, - body: getNoteSummary(data), - icon: data.user.avatarUrl + '?thumbnail&size=64' - }; - - case 'reaction': - return { - title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`, - body: getNoteSummary(data.note), - icon: data.user.avatarUrl + '?thumbnail&size=64' - }; - case 'unread_messaging_message': return { title: `${getUserName(data.user)}さんからメッセージ:`, @@ -62,6 +34,40 @@ export default function(type, data): Notification { icon: data.parent.avatarUrl + '?thumbnail&size=64' }; + case 'notification': + switch (data.type) { + case 'mention': + return { + title: `${getUserName(data.user)}さんから:`, + body: getNoteSummary(data), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + case 'reply': + return { + title: `${getUserName(data.user)}さんから返信:`, + body: getNoteSummary(data), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + case 'quote': + return { + title: `${getUserName(data.user)}さんが引用:`, + body: getNoteSummary(data), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + case 'reaction': + return { + title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`, + body: getNoteSummary(data.note), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + default: + return null; + } + default: return null; } diff --git a/src/client/app/common/scripts/get-kao.ts b/src/client/app/common/scripts/get-kao.ts index 2168c5be8..d38018751 100644 --- a/src/client/app/common/scripts/get-kao.ts +++ b/src/client/app/common/scripts/get-kao.ts @@ -1,5 +1,5 @@ export default () => [ '(=^・・^=)', - 'v(‘ω’)v', + 'v('ω')v', '🐡( \'-\' 🐡 )フグパンチ!!!!' ][Math.floor(Math.random() * 3)]; diff --git a/src/client/app/common/views/components/google.vue b/src/client/app/common/views/components/google.vue index 92817d3c1..8272961ef 100644 --- a/src/client/app/common/views/components/google.vue +++ b/src/client/app/common/views/components/google.vue @@ -1,7 +1,7 @@ <template> <div class="mk-google"> <input type="search" v-model="query" :placeholder="q"> - <button @click="search">検索</button> + <button @click="search">%fa:search% %i18n:common.search%</button> </div> </template> diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index 5b2fa084f..e2cba2e53 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -9,7 +9,7 @@ import forkit from './forkit.vue'; import acct from './acct.vue'; import avatar from './avatar.vue'; import nav from './nav.vue'; -import noteHtml from './note-html'; +import misskeyFlavoredMarkdown from './misskey-flavored-markdown'; import poll from './poll.vue'; import pollEditor from './poll-editor.vue'; import reactionIcon from './reaction-icon.vue'; @@ -47,7 +47,7 @@ Vue.component('mk-forkit', forkit); Vue.component('mk-acct', acct); Vue.component('mk-avatar', avatar); Vue.component('mk-nav', nav); -Vue.component('mk-note-html', noteHtml); +Vue.component('misskey-flavored-markdown', misskeyFlavoredMarkdown); Vue.component('mk-poll', poll); Vue.component('mk-poll-editor', pollEditor); Vue.component('mk-reaction-icon', reactionIcon); 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 a77b5f365..f33173da6 100644 --- a/src/client/app/common/views/components/messaging-room.message.vue +++ b/src/client/app/common/views/components/messaging-room.message.vue @@ -8,7 +8,7 @@ <img src="/assets/desktop/messaging/delete.png" alt="Delete"/> </button> <div class="content" v-if="!message.isDeleted"> - <mk-note-html class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/> + <misskey-flavored-markdown class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/> <div class="file" v-if="message.file"> <a :href="message.file.url" target="_blank" :title="message.file.name"> <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/> @@ -32,7 +32,7 @@ <script lang="ts"> import Vue from 'vue'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; export default Vue.extend({ props: { diff --git a/src/client/app/common/views/components/note-html.ts b/src/client/app/common/views/components/misskey-flavored-markdown.ts similarity index 89% rename from src/client/app/common/views/components/note-html.ts rename to src/client/app/common/views/components/misskey-flavored-markdown.ts index 8fa5f380d..c321c7610 100644 --- a/src/client/app/common/views/components/note-html.ts +++ b/src/client/app/common/views/components/misskey-flavored-markdown.ts @@ -1,6 +1,6 @@ import Vue from 'vue'; import * as emojilib from 'emojilib'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; import getAcct from '../../../../../acct/render'; import { url } from '../../../config'; import MkUrl from './url.vue'; @@ -10,7 +10,7 @@ const flatten = list => list.reduce( (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [] ); -export default Vue.component('mk-note-html', { +export default Vue.component('misskey-flavored-markdown', { props: { text: { type: String, @@ -40,17 +40,6 @@ export default Vue.component('mk-note-html', { ast = this.ast; } - if (ast.filter(x => x.type != 'hashtag').length == 0) { - return; - } - - while (ast[ast.length - 1] && ( - ast[ast.length - 1].type == 'hashtag' || - (ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') || - (ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n'))) { - ast.pop(); - } - // Parse ast to DOM const els = flatten(ast.map(token => { switch (token.type) { diff --git a/src/client/app/common/views/components/note-header.vue b/src/client/app/common/views/components/note-header.vue index 6e64a6a6d..25a333926 100644 --- a/src/client/app/common/views/components/note-header.vue +++ b/src/client/app/common/views/components/note-header.vue @@ -72,6 +72,7 @@ root(isDark) > .is-admin > .is-bot > .is-cat + flex-shrink 0 align-self center margin 0 .5em 0 0 padding 1px 6px @@ -89,6 +90,7 @@ root(isDark) overflow hidden text-overflow ellipsis color isDark ? #606984 : #ccc + flex-shrink 2147483647 > .info margin-left auto diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue index 0db6f66b3..ed7aedb58 100644 --- a/src/client/app/common/views/components/reaction-picker.vue +++ b/src/client/app/common/views/components/reaction-picker.vue @@ -1,7 +1,7 @@ <template> <div class="mk-reaction-picker"> <div class="backdrop" ref="backdrop" @click="close"></div> - <div class="popover" :class="{ compact }" ref="popover"> + <div class="popover" :class="{ compact, big }" ref="popover"> <p v-if="!compact">{{ title }}</p> <div> <button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button> @@ -25,7 +25,28 @@ import * as anime from 'animejs'; const placeholder = '%i18n:@choose-reaction%'; export default Vue.extend({ - props: ['note', 'source', 'compact', 'cb'], + props: { + note: { + type: Object, + required: true + }, + source: { + required: true + }, + compact: { + type: Boolean, + required: false, + default: false + }, + cb: { + required: false + }, + big: { + type: Boolean, + required: false, + default: false + } + }, data() { return { title: placeholder @@ -162,6 +183,16 @@ root(isDark) border-right solid $balloon-size transparent border-bottom solid $balloon-size $bgcolor + &.compact + > div + width 280px + + > button + width 50px + height 50px + font-size 28px + border-radius 4px + > p display block margin 0 diff --git a/src/client/app/common/views/components/reversi.game.vue b/src/client/app/common/views/components/reversi.game.vue index dc79c95bb..a2a6fd0dc 100644 --- a/src/client/app/common/views/components/reversi.game.vue +++ b/src/client/app/common/views/components/reversi.game.vue @@ -3,24 +3,39 @@ <header><b>{{ blackUser.name }}</b>(黒) vs <b>{{ whiteUser.name }}</b>(白)</header> <div style="overflow: hidden"> - <p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ turnUser.name }}のターンです<mk-ellipsis/></p> - <p class="turn" v-if="logPos != logs.length">{{ turnUser.name }}のターン</p> - <p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">相手のターンです<mk-ellipsis/></p> - <p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">あなたのターンです</p> + <p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ '%i18n:common.reversi.turn-of%'.replace('{}', turnUser.name) }}<mk-ellipsis/></p> + <p class="turn" v-if="logPos != logs.length">{{ '%i18n:common.reversi.past-turn-of%'.replace('{}', turnUser.name) }}</p> + <p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">%i18n:common.reversi.opponent-turn%<mk-ellipsis/></p> + <p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">%i18n:common.reversi.my-turn%</p> <p class="result" v-if="game.isEnded && logPos == logs.length"> <template v-if="game.winner"><b>{{ game.winner.name }}</b>の勝ち{{ game.settings.isLlotheo ? ' (ロセオ)' : '' }}</template> - <template v-else>引き分け</template> + <template v-else>%i18n:common.reversi.drawn%</template> </p> </div> - <div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }"> - <div v-for="(stone, i) in o.board" - :class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }" - @click="set(i)" - :title="'[' + (o.transformPosToXy(i)[0] + 1) + ', ' + (o.transformPosToXy(i)[1] + 1) + '] (' + i + ')'" - > - <img v-if="stone === true" :src="`${blackUser.avatarUrl}?thumbnail&size=128`" alt=""> - <img v-if="stone === false" :src="`${whiteUser.avatarUrl}?thumbnail&size=128`" alt=""> + <div class="board"> + <div class="labels-x" v-if="this.$store.state.settings.reversiBoardLabels"> + <span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span> + </div> + <div class="flex"> + <div class="labels-y" v-if="this.$store.state.settings.reversiBoardLabels"> + <div v-for="i in game.settings.map.length">{{ i }}</div> + </div> + <div class="cells" :style="cellsStyle"> + <div v-for="(stone, i) in o.board" + :class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }" + @click="set(i)" + :title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"> + <img v-if="stone === true" :src="`${blackUser.avatarUrl}?thumbnail&size=128`" alt=""> + <img v-if="stone === false" :src="`${whiteUser.avatarUrl}?thumbnail&size=128`" alt=""> + </div> + </div> + <div class="labels-y" v-if="this.$store.state.settings.reversiBoardLabels"> + <div v-for="i in game.settings.map.length">{{ i }}</div> + </div> + </div> + <div class="labels-x" v-if="this.$store.state.settings.reversiBoardLabels"> + <span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span> </div> </div> @@ -92,6 +107,12 @@ export default Vue.extend({ isMyTurn(): boolean { if (this.turnUser == null) return null; return this.turnUser.id == this.$store.state.i.id; + }, + cellsStyle(): any { + return { + 'grid-template-rows': `repeat(${ this.game.settings.map.length }, 1fr)`, + 'grid-template-columns': `repeat(${ this.game.settings.map[0].length }, 1fr)` + }; } }, @@ -244,54 +265,99 @@ export default Vue.extend({ border-bottom dashed 1px #c4cdd4 > .board - display grid - grid-gap 4px - width 350px - height 350px + width calc(100% - 16px) + max-width 500px margin 0 auto - > div - background transparent - border-radius 6px - overflow hidden + $label-size = 16px + $gap = 4px - * - pointer-events none - user-select none + > .labels-x + height $label-size + padding 0 $label-size + display flex - &.empty - border solid 2px #eee + > * + flex 1 + display flex + align-items center + justify-content center + font-size 12px - &.empty.can - background #eee + &:first-child + margin-left -($gap / 2) - &.empty.myTurn - border-color #ddd + &:last-child + margin-right -($gap / 2) - &.can - background #eee - cursor pointer + > .flex + display flex - &:hover - border-color darken($theme-color, 10%) - background $theme-color + > .labels-y + width $label-size + display flex + flex-direction column - &:active - background darken($theme-color, 10%) + > * + flex 1 + display flex + align-items center + justify-content center + font-size 12px - &.prev - box-shadow 0 0 0 4px rgba($theme-color, 0.7) + &:first-child + margin-top -($gap / 2) - &.isEnded - border-color #ddd + &:last-child + margin-bottom -($gap / 2) - &.none - border-color transparent !important + > .cells + flex 1 + display grid + grid-gap $gap - > img - display block - width 100% - height 100% + > div + background transparent + border-radius 6px + overflow hidden + + * + pointer-events none + user-select none + + &.empty + border solid 2px #eee + + &.empty.can + background #eee + + &.empty.myTurn + border-color #ddd + + &.can + background #eee + cursor pointer + + &:hover + border-color darken($theme-color, 10%) + background $theme-color + + &:active + background darken($theme-color, 10%) + + &.prev + box-shadow 0 0 0 4px rgba($theme-color, 0.7) + + &.isEnded + border-color #ddd + + &.none + border-color transparent !important + + > img + display block + width 100% + height 100% > .graph display grid diff --git a/src/client/app/common/views/components/reversi.vue b/src/client/app/common/views/components/reversi.vue index e4d7740bd..61705163a 100644 --- a/src/client/app/common/views/components/reversi.vue +++ b/src/client/app/common/views/components/reversi.vue @@ -10,7 +10,7 @@ </div> </div> <div class="index" v-else> - <h1>Misskey %fa:circle%thell%fa:circle R%</h1> + <h1>Misskey Reversi</h1> <p>他のMisskeyユーザーとリバーシで対戦しよう</p> <div class="play"> <el-button round>フリーマッチ(準備中)</el-button> diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue index f3372bf06..5a8b9df47 100644 --- a/src/client/app/common/views/components/welcome-timeline.vue +++ b/src/client/app/common/views/components/welcome-timeline.vue @@ -13,7 +13,7 @@ </div> </header> <div class="text"> - <mk-note-html v-if="note.text" :text="note.text"/> + <misskey-flavored-markdown v-if="note.text" :text="note.text"/> </div> </div> </div> @@ -24,6 +24,13 @@ import Vue from 'vue'; export default Vue.extend({ + props: { + max: { + type: Number, + required: false, + default: undefined + } + }, data() { return { fetching: true, @@ -37,6 +44,7 @@ export default Vue.extend({ fetch(cb?) { this.fetching = true; (this as any).api('notes', { + limit: this.max, local: true, reply: false, renote: false, diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue new file mode 100644 index 000000000..c8e838be8 --- /dev/null +++ b/src/client/app/common/views/pages/follow.vue @@ -0,0 +1,215 @@ +<template> +<div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching" :data-darkmode="$store.state.device.darkmode"> + <div class="signed-in-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + myName + '</b>')"></div> + + <main> + <div class="banner" :style="bannerStyle"></div> + <mk-avatar class="avatar" :user="user" :disable-preview="true"/> + <div class="body"> + <router-link :to="user | userPage" class="name">{{ user | userName }}</router-link> + <span class="username">@{{ user | acct }}</span> + <div class="description"> + <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/> + </div> + </div> + </main> + + <button + :class="{ wait: followWait, active: user.isFollowing || user.hasPendingFollowRequestFromYou }" + @click="onClick" + :disabled="followWait"> + <template v-if="!followWait"> + <template v-if="user.hasPendingFollowRequestFromYou">%fa:hourglass-half% %i18n:@request-pending%</template> + <template v-else-if="user.isFollowing">%fa:minus% %i18n:@following%</template> + <template v-else-if="!user.isFollowing && user.isLocked">%fa:plus% %i18n:@follow-request%</template> + <template v-else-if="!user.isFollowing && !user.isLocked">%fa:plus% %i18n:@follow%</template> + </template> + <template v-else>%fa:spinner .pulse .fw%</template> + </button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import parseAcct from '../../../../../acct/parse'; +import getUserName from '../../../../../renderers/get-user-name'; +import Progress from '../../../common/scripts/loading'; + +export default Vue.extend({ + data() { + return { + fetching: true, + user: null, + followWait: false + }; + }, + + computed: { + myName(): string { + return Vue.filter('userName')(this.$store.state.i); + }, + + bannerStyle(): any { + if (this.user.bannerUrl == null) return {}; + return { + backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null, + backgroundImage: `url(${ this.user.bannerUrl })` + }; + } + }, + + created() { + this.fetch(); + }, + + methods: { + fetch() { + const acct = new URL(location.href).searchParams.get('acct'); + this.fetching = true; + Progress.start(); + (this as any).api('users/show', parseAcct(acct)).then(user => { + this.user = user; + this.fetching = false; + Progress.done(); + document.title = getUserName(this.user) + ' | Misskey'; + }); + }, + + async onClick() { + this.followWait = true; + + try { + if (this.user.isFollowing) { + this.user = await (this as any).api('following/delete', { + userId: this.user.id + }); + } else { + if (this.user.isLocked && this.user.hasPendingFollowRequestFromYou) { + this.user = await (this as any).api('following/requests/cancel', { + userId: this.user.id + }); + } else if (this.user.isLocked) { + this.user = await (this as any).api('following/create', { + userId: this.user.id + }); + } else { + this.user = await (this as any).api('following/create', { + userId: this.user.id + }); + } + } + } catch (e) { + console.error(e); + } finally { + this.followWait = false; + } + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +root(isDark) + padding 32px + max-width 500px + margin 0 auto + text-align center + color isDark ? #9baec8 : #868c8c + + $bg = isDark ? #282C37 : #fff + + @media (max-width 400px) + padding 16px + + > .signed-in-as + margin-bottom 16px + font-size 14px + color isDark ? #9baec8 : #9daab3 + + > main + margin-bottom 16px + background $bg + border-radius 8px + box-shadow 0 4px 12px rgba(#000, 0.1) + overflow hidden + + > .banner + height 128px + background-position center + background-size cover + + > .avatar + display block + margin -50px auto 0 auto + width 100px + height 100px + border-radius 100% + border solid 4px $bg + + > .body + padding 4px 32px 32px 32px + + @media (max-width 400px) + padding 4px 16px 16px 16px + + > .name + font-size 20px + font-weight bold + + > .username + display block + opacity 0.7 + + > .description + margin-top 16px + + > button + display block + user-select none + cursor pointer + padding 10px 16px + margin 0 + width 100% + min-width 150px + font-size 14px + font-weight bold + color $theme-color + background transparent + outline none + border solid 1px $theme-color + border-radius 36px + + &:hover + background rgba($theme-color, 0.1) + + &:active + background rgba($theme-color, 0.2) + + &.active + color $theme-color-foreground + background $theme-color + + &:hover + background lighten($theme-color, 10%) + border-color lighten($theme-color, 10%) + + &:active + background darken($theme-color, 10%) + border-color darken($theme-color, 10%) + + &.wait + cursor wait !important + opacity 0.7 + + * + pointer-events none + +.syxhndwprovvuqhmyvveewmbqayniwkv[data-darkmode] + root(true) + +.syxhndwprovvuqhmyvveewmbqayniwkv:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/common/views/widgets/hashtags.vue b/src/client/app/common/views/widgets/hashtags.vue index 9ab855d92..2065bd407 100644 --- a/src/client/app/common/views/widgets/hashtags.vue +++ b/src/client/app/common/views/widgets/hashtags.vue @@ -6,7 +6,9 @@ <div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'"> <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> <p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p> - <transition-group v-else tag="div" name="chart"> + <!-- トランジションを有効にするとなぜかメモリリークする --> + <!-- <transition-group v-else tag="div" name="chart"> --> + <div> <div v-for="stat in stats" :key="stat.tag"> <div class="tag"> <router-link :to="`/tags/${ stat.tag }`" :title="stat.tag">#{{ stat.tag }}</router-link> @@ -14,7 +16,8 @@ </div> <x-chart class="chart" :src="stat.chart"/> </div> - </transition-group> + </div> + <!-- </transition-group> --> </div> </mk-widget-container> </div> diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index 201ab0a83..297100e0e 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -36,6 +36,7 @@ import MkSearch from './views/pages/search.vue'; import MkTag from './views/pages/tag.vue'; import MkReversi from './views/pages/reversi.vue'; import MkShare from './views/pages/share.vue'; +import MkFollow from '../common/views/pages/follow.vue'; /** * init @@ -67,7 +68,8 @@ init(async (launch) => { { path: '/reversi', component: MkReversi }, { path: '/reversi/:game', component: MkReversi }, { path: '/@:user', component: MkUser }, - { path: '/notes/:note', component: MkNote } + { path: '/notes/:note', component: MkNote }, + { path: '/authorize-follow', component: MkFollow } ] }); @@ -115,6 +117,15 @@ function registerNotifications(stream: HomeStreamManager) { }); function attach(connection) { + connection.on('notification', notification => { + const _n = composeNotification('notification', notification); + const n = new Notification(_n.title, { + body: _n.body, + icon: _n.icon + }); + setTimeout(n.close.bind(n), 6000); + }); + connection.on('drive_file_created', file => { const _n = composeNotification('drive_file_created', file); const n = new Notification(_n.title, { @@ -124,33 +135,6 @@ function registerNotifications(stream: HomeStreamManager) { setTimeout(n.close.bind(n), 5000); }); - connection.on('mention', note => { - const _n = composeNotification('mention', note); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - setTimeout(n.close.bind(n), 6000); - }); - - connection.on('reply', note => { - const _n = composeNotification('reply', note); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - setTimeout(n.close.bind(n), 6000); - }); - - connection.on('quote', note => { - const _n = composeNotification('quote', note); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - setTimeout(n.close.bind(n), 6000); - }); - connection.on('unread_messaging_message', message => { const _n = composeNotification('unread_messaging_message', message); const n = new Notification(_n.title, { diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue index ba48ce24e..ab276d3c4 100644 --- a/src/client/app/desktop/views/components/home.vue +++ b/src/client/app/desktop/views/components/home.vue @@ -66,7 +66,7 @@ </div> <div class="main"> <mk-post-form class="form" v-if="$store.state.settings.showPostFormOnTopOfTl"/> - <mk-timeline class="tl" cref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/> + <mk-timeline class="tl" ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/> </div> </template> </div> diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue index 4b5e5bebd..e3d16d705 100644 --- a/src/client/app/desktop/views/components/note-detail.vue +++ b/src/client/app/desktop/views/components/note-detail.vue @@ -40,16 +40,13 @@ <div class="text"> <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span> <span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span> - <mk-note-html v-if="p.text" :text="p.text" :i="$store.state.i"/> + <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/> </div> <div class="media" v-if="p.media.length > 0"> <mk-media-list :media-list="p.media" :raw="true"/> </div> <mk-poll v-if="p.poll" :note="p"/> <mk-url-preview v-for="url in urls" :url="url" :key="url"/> - <div class="tags" v-if="p.tags && p.tags.length > 0"> - <router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${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% %i18n:@location%</a> <div class="map" v-if="p.geo" ref="map"></div> <div class="renote" v-if="p.renote"> @@ -83,7 +80,7 @@ <script lang="ts"> import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; import MkPostFormWindow from './post-form-window.vue'; import MkRenoteFormWindow from './renote-form-window.vue'; @@ -363,35 +360,6 @@ root(isDark) > .mk-url-preview margin-top 8px - > .tags - margin 4px 0 0 0 - - > * - display inline-block - margin 0 8px 0 0 - padding 2px 8px 2px 16px - font-size 90% - color #8d969e - background #edf0f3 - border-radius 4px - - &:before - content "" - display block - position absolute - top 0 - bottom 0 - left 4px - width 8px - height 8px - margin auto 0 - background #fff - border-radius 100% - - &:hover - text-decoration none - background #e2e7ec - > footer font-size 1.2em diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue index ee11fcc55..b8aff2d86 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -25,16 +25,13 @@ <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span> <span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span> <a class="reply" v-if="p.reply">%fa:reply%</a> - <mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="$store.state.i" :class="$style.text"/> + <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/> <a class="rp" v-if="p.renote">RP:</a> </div> <div class="media" v-if="p.media.length > 0"> <mk-media-list :media-list="p.media"/> </div> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> - <div class="tags" v-if="p.tags && p.tags.length > 0"> - <router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${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> <div class="map" v-if="p.geo" ref="map"></div> <div class="renote" v-if="p.renote"> @@ -75,8 +72,7 @@ <script lang="ts"> import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; -import canHideText from '../../../common/scripts/can-hide-text'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; import MkPostFormWindow from './post-form-window.vue'; import MkRenoteFormWindow from './renote-form-window.vue'; @@ -190,8 +186,6 @@ export default Vue.extend({ }, methods: { - canHideText, - capture(withHandler = false) { if (this.$store.getters.isSignedIn) { this.connection.send({ @@ -468,35 +462,6 @@ root(isDark) &:empty display none - > .tags - margin 4px 0 0 0 - - > * - display inline-block - margin 0 8px 0 0 - padding 2px 8px 2px 16px - font-size 90% - color #8d969e - background isDark ? #313543 : #edf0f3 - border-radius 4px - - &:before - content "" - display block - position absolute - top 0 - bottom 0 - left 4px - width 8px - height 8px - margin auto 0 - background isDark ? #282c37 : #fff - border-radius 100% - - &:hover - text-decoration none - background #e2e7ec - .mk-url-preview margin-top 8px diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index 69f3739f7..1206eb713 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -9,7 +9,9 @@ <button @click="resolveInitPromise">%i18n:@retry%</button> </div> - <transition-group name="mk-notes" class="transition"> + <!-- トランジションを有効にするとなぜかメモリリークする --> + <!--<transition-group name="mk-notes" class="transition">--> + <div class="notes"> <template v-for="(note, i) in _notes"> <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> @@ -17,7 +19,8 @@ <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> </p> </template> - </transition-group> + </div> + <!--</transition-group>--> <footer v-if="more"> <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> @@ -221,6 +224,7 @@ root(isDark) > * transition transform .3s ease, opacity .3s ease + > .notes > .date display block margin 0 diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue index e479ffadb..32b36994d 100644 --- a/src/client/app/desktop/views/components/notifications.vue +++ b/src/client/app/desktop/views/components/notifications.vue @@ -1,7 +1,9 @@ <template> <div class="mk-notifications"> <div class="notifications" v-if="notifications.length != 0"> - <transition-group name="mk-notifications" class="transition"> + <!-- トランジションを有効にするとなぜかメモリリークする --> + <!-- <transition-group name="mk-notifications" class="transition"> --> + <div> <template v-for="(notification, i) in _notifications"> <div class="notification" :class="notification.type" :key="notification.id"> <mk-time :time="notification.createdAt"/> @@ -95,7 +97,8 @@ <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> </p> </template> - </transition-group> + </div> + <!-- </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%' }} @@ -201,7 +204,7 @@ root(isDark) transition transform .3s ease, opacity .3s ease > .notifications - > * + > div > .notification margin 0 padding 16px diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue index 33f2288e0..3832e5b38 100644 --- a/src/client/app/desktop/views/components/post-form.vue +++ b/src/client/app/desktop/views/components/post-form.vue @@ -49,7 +49,7 @@ import Vue from 'vue'; import * as XDraggable from 'vuedraggable'; import getKao from '../../../common/scripts/get-kao'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; import { host } from '../../../config'; export default Vue.extend({ diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index 536d270dc..74ab45626 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -54,6 +54,7 @@ <mk-switch v-model="$store.state.settings.showMaps" @change="onChangeShowMaps" text="%i18n:@show-maps%"> <span>%i18n:@show-maps-desc%</span> </mk-switch> + <mk-switch v-model="$store.state.settings.reversiBoardLabels" @change="onChangeReversiBoardLabels" text="%i18n:common.show-reversi-board-labels%"/> </section> <section class="web" v-show="page == 'web'"> @@ -369,6 +370,12 @@ export default Vue.extend({ value: v }); }, + onChangeReversiBoardLabels(v) { + this.$store.dispatch('settings/set', { + key: 'reversiBoardLabels', + value: v + }); + }, onChangeGradientWindowHeader(v) { this.$store.dispatch('settings/set', { key: 'gradientWindowHeader', 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 45ce6a6f8..cb0374b91 100644 --- a/src/client/app/desktop/views/components/sub-note-content.vue +++ b/src/client/app/desktop/views/components/sub-note-content.vue @@ -4,7 +4,7 @@ <span v-if="note.isHidden" style="opacity: 0.5">%i18n:@private%</span> <span v-if="note.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span> <a class="reply" v-if="note.replyId">%fa:reply%</a> - <mk-note-html v-if="note.text" :text="note.text" :i="$store.state.i"/> + <misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/> <a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RP: ...</a> </div> <details v-if="note.media.length > 0"> diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue index 704579005..aa7c3ac44 100644 --- a/src/client/app/desktop/views/components/ui.header.vue +++ b/src/client/app/desktop/views/components/ui.header.vue @@ -4,7 +4,7 @@ <div class="main" ref="main"> <div class="backdrop"></div> <div class="main"> - <p ref="welcomeback" v-if="$store.getters.isSignedIn">おかえりなさい、<b>{{ $store.state.i | userName }}</b>さん</p> + <p ref="welcomeback" v-if="$store.getters.isSignedIn">%i18n:@welcome-back%<b>{{ $store.state.i | userName }}</b>さん</p> <div class="container" ref="mainContainer"> <div class="left"> <x-nav/> diff --git a/src/client/app/desktop/views/pages/deck/deck.note.vue b/src/client/app/desktop/views/pages/deck/deck.note.vue index 5a8dc2ea6..c7df715a0 100644 --- a/src/client/app/desktop/views/pages/deck/deck.note.vue +++ b/src/client/app/desktop/views/pages/deck/deck.note.vue @@ -25,16 +25,13 @@ <span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span> <span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span> <a class="reply" v-if="p.reply">%fa:reply%</a> - <mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="$store.state.i"/> + <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/> <a class="rp" v-if="p.renote != null">RP:</a> </div> <div class="media" v-if="p.media.length > 0"> <mk-media-list :media-list="p.media"/> </div> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> - <div class="tags" v-if="p.tags && p.tags.length > 0"> - <router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${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% %i18n:@location%</a> <div class="renote" v-if="p.renote"> <mk-note-preview :note="p.renote" :mini="true"/> @@ -67,8 +64,7 @@ <script lang="ts"> import Vue from 'vue'; -import parse from '../../../../../../text/parse'; -import canHideText from '../../../../common/scripts/can-hide-text'; +import parse from '../../../../../../mfm/parse'; import MkNoteMenu from '../../../../common/views/components/note-menu.vue'; import MkReactionPicker from '../../../../common/views/components/reaction-picker.vue'; @@ -148,8 +144,6 @@ export default Vue.extend({ }, methods: { - canHideText, - capture(withHandler = false) { if (this.$store.getters.isSignedIn) { this.connection.send({ @@ -376,31 +370,6 @@ root(isDark) .mk-url-preview margin-top 8px - > .tags - margin 4px 0 0 0 - - > * - display inline-block - margin 0 8px 0 0 - padding 2px 8px 2px 16px - font-size 90% - color #8d969e - background isDark ? #313543 : #edf0f3 - border-radius 4px - - &:before - content "" - display block - position absolute - top 0 - bottom 0 - left 4px - width 8px - height 8px - margin auto 0 - background isDark ? #282c37 : #fff - border-radius 100% - > .media > img display block diff --git a/src/client/app/desktop/views/pages/deck/deck.notes.vue b/src/client/app/desktop/views/pages/deck/deck.notes.vue index 8862b0e0f..a5ed45b64 100644 --- a/src/client/app/desktop/views/pages/deck/deck.notes.vue +++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue @@ -7,7 +7,9 @@ <button @click="resolveInitPromise">%i18n:@retry%</button> </div> - <transition-group name="mk-notes" class="transition"> + <!-- トランジションを有効にするとなぜかメモリリークする --> + <!--<transition-group name="mk-notes" class="transition">--> + <div class="notes"> <template v-for="(note, i) in _notes"> <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :media-view="mediaView"/> <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> @@ -15,7 +17,8 @@ <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> </p> </template> - </transition-group> + </div> + <!--</transition-group>--> <footer v-if="more"> <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> @@ -198,6 +201,7 @@ root(isDark) > * transition transform .3s ease, opacity .3s ease + > .notes > .date display block margin 0 diff --git a/src/client/app/desktop/views/pages/deck/deck.notifications.vue b/src/client/app/desktop/views/pages/deck/deck.notifications.vue index f54ad1a3c..10c06b0ad 100644 --- a/src/client/app/desktop/views/pages/deck/deck.notifications.vue +++ b/src/client/app/desktop/views/pages/deck/deck.notifications.vue @@ -1,6 +1,8 @@ <template> <div class="oxynyeqmfvracxnglgulyqfgqxnxmehl"> - <transition-group name="mk-notifications" class="transition notifications"> + <!-- トランジションを有効にするとなぜかメモリリークする --> + <!--<transition-group name="mk-notifications" class="transition notifications">--> + <div class="notifications"> <template v-for="(notification, i) in _notifications"> <x-notification class="notification" :notification="notification" :key="notification.id"/> <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> @@ -8,7 +10,8 @@ <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> </p> </template> - </transition-group> + </div> + <!--</transition-group>--> <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%' }} </button> diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue index d52c6b762..00545723e 100644 --- a/src/client/app/desktop/views/pages/user/user.header.vue +++ b/src/client/app/desktop/views/pages/user/user.header.vue @@ -1,27 +1,37 @@ <template> <div class="header" :data-is-dark-background="user.bannerUrl != null"> - <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> <div class="banner-container" :style="style"> <div class="banner" ref="banner" :style="style" @click="onBannerClick"></div> <div class="fade"></div> - </div> - <div class="container"> - <mk-avatar class="avatar" :user="user" :disable-preview="true"/> <div class="title"> <p class="name">{{ user | userName }}</p> - <p class="username"><mk-acct :user="user"/></p> - <p class="location" v-if="user.host === null && user.profile.location">%fa:map-marker%{{ user.profile.location }}</p> + <div> + <span class="username"><mk-acct :user="user"/></span> + <span v-if="user.isBot" title="%i18n:@is-bot%">%fa:robot%</span> + <span class="location" v-if="user.host === null && user.profile.location">%fa:map-marker% {{ user.profile.location }}</span> + <span class="birthday" v-if="user.host === null && user.profile.birthday">%fa:birthday-cake% {{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</span> + </div> + </div> + </div> + <mk-avatar class="avatar" :user="user" :disable-preview="true"/> + <div class="body"> + <div class="description"> + <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/> + </div> + <div class="status"> + <span class="notes-count"><b>{{ user.notesCount | number }}</b>%i18n:@posts%</span> + <span class="following clickable" @click="showFollowing"><b>{{ user.followingCount | number }}</b>%i18n:@following%</span> + <span class="followers clickable" @click="showFollowers"><b>{{ user.followersCount | number }}</b>%i18n:@followers%</span> </div> - <footer> - <router-link :to="user | userPage" :data-active="$parent.page == 'home'">%fa:home%ホーム</router-link> - </footer> </div> </div> </template> <script lang="ts"> import Vue from 'vue'; +import MkFollowingWindow from '../../components/following-window.vue'; +import MkFollowersWindow from '../../components/followers-window.vue'; +import * as age from 's-age'; export default Vue.extend({ props: ['user'], @@ -32,20 +42,24 @@ export default Vue.extend({ backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null, backgroundImage: `url(${ this.user.bannerUrl })` }; + }, + + age(): number { + return age(this.user.profile.birthday); } }, mounted() { if (this.user.bannerUrl) { - window.addEventListener('load', this.onScroll); - window.addEventListener('scroll', this.onScroll, { passive: true }); - window.addEventListener('resize', this.onScroll); + //window.addEventListener('load', this.onScroll); + //window.addEventListener('scroll', this.onScroll, { passive: true }); + //window.addEventListener('resize', this.onScroll); } }, beforeDestroy() { if (this.user.bannerUrl) { - window.removeEventListener('load', this.onScroll); - window.removeEventListener('scroll', this.onScroll); - window.removeEventListener('resize', this.onScroll); + //window.removeEventListener('load', this.onScroll); + //window.removeEventListener('scroll', this.onScroll); + //window.removeEventListener('resize', this.onScroll); } }, methods: { @@ -68,7 +82,19 @@ export default Vue.extend({ (this as any).apis.updateBanner().then(i => { this.user.bannerUrl = i.bannerUrl; }); - } + }, + + showFollowing() { + (this as any).os.new(MkFollowingWindow, { + user: this.user + }); + }, + + showFollowers() { + (this as any).os.new(MkFollowersWindow, { + user: this.user + }); + }, } }); </script> @@ -76,31 +102,11 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.header - $footer-height = 58px - +root(isDark) + background isDark ? #282C37 : #fff + border 1px solid rgba(#000, 0.075) + border-radius 6px overflow hidden - background #f7f7f7 - box-shadow 0 1px 1px rgba(#000, 0.075) - - > .is-suspended - > .is-remote - &.is-suspended - color #570808 - background #ffdbdb - - &.is-remote - color #573c08 - background #fff0db - - > p - margin 0 auto - padding 14px 16px - max-width 1200px - font-size 14px - - > a - font-weight bold &[data-is-dark-background] > .banner-container @@ -110,7 +116,6 @@ export default Vue.extend({ > .fade background linear-gradient(transparent, rgba(#000, 0.7)) - > .container > .title color #fff @@ -118,7 +123,7 @@ export default Vue.extend({ text-shadow 0 0 8px #000 > .banner-container - height 320px + height 250px overflow hidden background-size cover background-position center @@ -136,83 +141,75 @@ export default Vue.extend({ width 100% height 78px - > .container - max-width 1200px - margin 0 auto - - > .avatar - display block - position absolute - bottom 16px - left 16px - z-index 2 - width 160px - height 160px - border solid 3px #fff - border-radius 8px - box-shadow 1px 1px 3px rgba(#000, 0.2) - > .title position absolute - bottom $footer-height + bottom 0 left 0 width 100% - padding 0 0 8px 195px + padding 0 0 8px 154px color #5e6367 - font-family '游ゴシック', 'YuGothic', 'ヒラギノ角ゴ ProN W3', 'Hiragino Kaku Gothic ProN', 'Meiryo', 'メイリオ', sans-serif > .name display block margin 0 - line-height 40px + line-height 32px font-weight bold - font-size 2em + font-size 1.8em - > .username - > .location + > div + > * + display inline-block + margin-right 16px + line-height 20px + opacity 0.8 + + &.username + font-weight bold + + > .avatar + display block + position absolute + top 170px + left 16px + z-index 2 + width 120px + height 120px + box-shadow 1px 1px 3px rgba(#000, 0.2) + + > .body + padding 16px 16px 16px 154px + color isDark ? #c5ced6 : #555 + + > .status + margin-top 16px + padding-top 16px + border-top solid 1px rgba(#000, isDark ? 0.2 : 0.1) + font-size 80% + + > * display inline-block - margin 0 16px 0 0 - line-height 20px - opacity 0.8 + padding-right 16px + margin-right 16px - > i + &:not(:last-child) + border-right solid 1px rgba(#000, isDark ? 0.2 : 0.1) + + &.clickable + cursor pointer + + &:hover + color isDark ? #fff : #000 + + > b margin-right 4px + font-size 1rem + font-weight bold + color $theme-color - > footer - z-index 1 - height $footer-height - padding-left 195px +.header[data-darkmode] + root(true) - > a - display inline-block - margin 0 - padding 0 16px - height $footer-height - line-height $footer-height - color #555 - - &[data-active] - border-bottom solid 4px $theme-color - - > i - margin-right 6px - - > button - display block - position absolute - top 0 - right 0 - margin 8px - padding 0 - width $footer-height - 16px - line-height $footer-height - 16px - 2px - font-size 1.2em - color #777 - border solid 1px #eee - border-radius 4px - - &:hover - color #555 - border solid 1px #ddd +.header:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/desktop/views/pages/user/user.home.vue b/src/client/app/desktop/views/pages/user/user.home.vue deleted file mode 100644 index afaf97dc9..000000000 --- a/src/client/app/desktop/views/pages/user/user.home.vue +++ /dev/null @@ -1,103 +0,0 @@ -<template> -<div class="home"> - <div> - <div ref="left"> - <x-profile :user="user"/> - <x-photos :user="user"/> - <x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/> - <p v-if="user.host === null">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p> - </div> - </div> - <main> - <mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/> - <x-timeline class="timeline" ref="tl" :user="user"/> - </main> - <div> - <div ref="right"> - <mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/> - <mk-activity :user="user"/> - <x-friends :user="user"/> - <div class="nav"><mk-nav/></div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XTimeline from './user.timeline.vue'; -import XProfile from './user.profile.vue'; -import XPhotos from './user.photos.vue'; -import XFollowersYouKnow from './user.followers-you-know.vue'; -import XFriends from './user.friends.vue'; - -export default Vue.extend({ - components: { - XTimeline, - XProfile, - XPhotos, - XFollowersYouKnow, - XFriends - }, - props: ['user'], - methods: { - warp(date) { - (this.$refs.tl as any).warp(date); - } - } -}); -</script> - -<style lang="stylus" scoped> -.home - display flex - justify-content center - margin 0 auto - max-width 1200px - - > main - > div > div - > *:not(:last-child) - margin-bottom 16px - - > main - padding 16px - width calc(100% - 275px * 2) - - > .timeline - border solid 1px rgba(#000, 0.075) - border-radius 6px - - > div - width 275px - margin 0 - - &:first-child > div - padding 16px 0 16px 16px - - > p - display block - margin 0 - padding 0 12px - text-align center - font-size 0.8em - color #aaa - - &:last-child > div - padding 16px 16px 16px 0 - - > .nav - padding 16px - font-size 12px - color #aaa - background #fff - border solid 1px rgba(#000, 0.075) - border-radius 6px - - a - color #999 - - i - color #ccc - -</style> diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue index 5aa08f7c8..0134d6f0b 100644 --- a/src/client/app/desktop/views/pages/user/user.profile.vue +++ b/src/client/app/desktop/views/pages/user/user.profile.vue @@ -15,48 +15,17 @@ </button> <button class="mute ui" @click="list">%fa:list% リストに追加</button> </div> - <div class="description" v-if="user.description">{{ user.description }}</div> - <div class="birthday" v-if="user.host === null && user.profile.birthday"> - <p>%fa:birthday-cake%{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</p> - </div> - <div class="twitter" v-if="user.host === null && user.twitter"> - <p>%fa:B twitter%<a :href="`https://twitter.com/${user.twitter.screenName}`" target="_blank">@{{ user.twitter.screenName }}</a></p> - </div> - <div class="status"> - <p class="notes-count">%fa:angle-right%<a>{{ user.notesCount }}</a><b>投稿</b></p> - <p class="following">%fa:angle-right%<a @click="showFollowing">{{ user.followingCount }}</a>人を<b>フォロー</b></p> - <p class="followers">%fa:angle-right%<a @click="showFollowers">{{ user.followersCount }}</a>人の<b>フォロワー</b></p> - </div> </div> </template> <script lang="ts"> import Vue from 'vue'; -import * as age from 's-age'; -import MkFollowingWindow from '../../components/following-window.vue'; -import MkFollowersWindow from '../../components/followers-window.vue'; import MkUserListsWindow from '../../components/user-lists-window.vue'; export default Vue.extend({ props: ['user'], - computed: { - age(): number { - return age(this.user.profile.birthday); - } - }, + methods: { - showFollowing() { - (this as any).os.new(MkFollowingWindow, { - user: this.user - }); - }, - - showFollowers() { - (this as any).os.new(MkFollowersWindow, { - user: this.user - }); - }, - stalk() { (this as any).api('following/stalk', { userId: this.user.id @@ -116,8 +85,8 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.profile - background #fff +root(isDark) + background isDark ? #282C37 : #fff border solid 1px rgba(#000, 0.075) border-radius 6px @@ -127,7 +96,7 @@ export default Vue.extend({ > .friend-form padding 16px text-align center - border-top solid 1px #eee + border-bottom solid 1px isDark ? #21242f : #eee > .followed margin 12px 0 0 0 @@ -145,7 +114,7 @@ export default Vue.extend({ > .action-form padding 16px text-align center - border-top solid 1px #eee + border-bottom solid 1px isDark ? #21242f : #eee > * width 100% @@ -153,43 +122,10 @@ export default Vue.extend({ &:not(:last-child) margin-bottom 12px - > .description - padding 16px - color #555 - border-top solid 1px #eee +.profile[data-darkmode] + root(true) - > .birthday - padding 16px - color #555 - border-top solid 1px #eee - - > p - margin 0 - - > i - margin-right 8px - - > .twitter - padding 16px - color #555 - border-top solid 1px #eee - - > p - margin 0 - - > i - margin-right 8px - - > .status - padding 16px - color #555 - border-top solid 1px #eee - - > p - margin 8px 0 - - > i - margin-left 8px - margin-right 8px +.profile:not([data-darkmode]) + root(false) </style> 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 812b5b422..67987fcb9 100644 --- a/src/client/app/desktop/views/pages/user/user.timeline.vue +++ b/src/client/app/desktop/views/pages/user/user.timeline.vue @@ -1,9 +1,9 @@ <template> -<div class="timeline"> +<div class="oh5y2r7l5lx8j6jj791ykeiwgihheguk"> <header> - <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> + <span :data-active="mode == 'default'" @click="mode = 'default'">%fa:comment-alt R% %i18n:@default%</span> + <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">%fa:comments% %i18n:@with-replies%</span> + <span :data-active="mode == 'with-media'" @click="mode = 'with-media'">%fa:images% %i18n:@with-media%</span> </header> <div class="loading" v-if="fetching"> <mk-ellipsis-icon/> @@ -114,25 +114,44 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.timeline - background #fff +root(isDark) + background isDark ? #282C37 : #fff > header - padding 8px 16px - border-bottom solid 1px #eee + padding 0 8px + z-index 10 + background isDark ? #313543 : #fff + border-radius 6px 6px 0 0 + box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08) > span - margin-right 16px - line-height 27px - font-size 18px - color #555 + display inline-block + padding 0 10px + line-height 42px + font-size 12px + user-select none + + &[data-active] + color $theme-color + cursor default + font-weight bold + + &:before + content "" + display block + position absolute + bottom 0 + left -8px + width calc(100% + 16px) + height 2px + background $theme-color &:not([data-active]) - color $theme-color + color isDark ? #9aa2a7 : #6f7477 cursor pointer &:hover - text-decoration underline + color isDark ? #d9dcde : #525a5f > .loading padding 64px 0 @@ -151,4 +170,10 @@ export default Vue.extend({ font-size 3em color #ccc +.oh5y2r7l5lx8j6jj791ykeiwgihheguk[data-darkmode] + root(true) + +.oh5y2r7l5lx8j6jj791ykeiwgihheguk:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/pages/user/user.twitter.vue b/src/client/app/desktop/views/pages/user/user.twitter.vue new file mode 100644 index 000000000..228ce1de9 --- /dev/null +++ b/src/client/app/desktop/views/pages/user/user.twitter.vue @@ -0,0 +1,26 @@ +<template> +<div class="adsvaidqfznoartcbplullnejvxjphcn"> + <span>%fa:B twitter%<a :href="`https://twitter.com/${user.twitter.screenName}`" target="_blank">@{{ user.twitter.screenName }}</a></span> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['user'] +}); +</script> + +<style lang="stylus" scoped> +.adsvaidqfznoartcbplullnejvxjphcn + padding 32px + background #1a94f2 + border-radius 6px + color #fff + + a + margin-left 8px + color #fff + +</style> diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue index 3644286fb..fc5c90003 100644 --- a/src/client/app/desktop/views/pages/user/user.vue +++ b/src/client/app/desktop/views/pages/user/user.vue @@ -1,8 +1,26 @@ <template> <mk-ui> - <div class="user" v-if="!fetching"> - <x-header :user="user"/> - <x-home v-if="page == 'home'" :user="user"/> + <div class="xygkxeaeontfaokvqmiblezmhvhostak" v-if="!fetching" :data-darkmode="$store.state.device.darkmode"> + <div class="is-suspended" v-if="user.isSuspended">%fa:exclamation-triangle% %i18n:@is-suspended%</div> + <div class="is-remote" v-if="user.host != null">%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></div> + <main> + <div class="main"> + <x-header :user="user"/> + <mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/> + <x-timeline class="timeline" ref="tl" :user="user"/> + </div> + <div class="side"> + <x-profile :user="user"/> + <x-twitter :user="user" v-if="user.host === null && user.twitter"/> + <mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/> + <mk-activity :user="user"/> + <x-photos :user="user"/> + <x-friends :user="user"/> + <x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/> + <div class="nav"><mk-nav/></div> + <p v-if="user.host === null">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p> + </div> + </main> </div> </mk-ui> </template> @@ -13,17 +31,22 @@ import parseAcct from '../../../../../../acct/parse'; import getUserName from '../../../../../../renderers/get-user-name'; import Progress from '../../../../common/scripts/loading'; import XHeader from './user.header.vue'; -import XHome from './user.home.vue'; +import XTimeline from './user.timeline.vue'; +import XProfile from './user.profile.vue'; +import XPhotos from './user.photos.vue'; +import XFollowersYouKnow from './user.followers-you-know.vue'; +import XFriends from './user.friends.vue'; +import XTwitter from './user.twitter.vue'; export default Vue.extend({ components: { XHeader, - XHome - }, - props: { - page: { - default: 'home' - } + XTimeline, + XProfile, + XPhotos, + XFollowersYouKnow, + XFriends, + XTwitter }, data() { return { @@ -47,8 +70,89 @@ export default Vue.extend({ Progress.done(); document.title = getUserName(this.user) + ' | Misskey'; }); + }, + + warp(date) { + (this.$refs.tl as any).warp(date); } } }); </script> +<style lang="stylus" scoped> +root(isDark) + width 980px + padding 16px + margin 0 auto + + > .is-suspended + > .is-remote + margin-bottom 16px + padding 14px 16px + font-size 14px + border-radius 6px + + &.is-suspended + color isDark ? #ffb4b4 : #570808 + background isDark ? #611d1d : #ffdbdb + border solid 1px isDark ? #d64a4a : #e09696 + + &.is-remote + color isDark ? #ffbd3e : #573c08 + background isDark ? #42321c : #fff0db + border solid 1px isDark ? #90733c : #dcbb7b + + > a + font-weight bold + + > main + display flex + justify-content center + + > .main + > .side + > *:not(:last-child) + margin-bottom 16px + + > .main + flex 1 + min-width 0 // SEE: http://kudakurage.hatenadiary.com/entry/2016/04/01/232722 + margin-right 16px + + > .timeline + border 1px solid rgba(#000, 0.075) + border-radius 6px + + > .side + width 275px + flex-shrink 0 + + > p + display block + margin 0 + padding 0 12px + text-align center + font-size 0.8em + color #aaa + + > .nav + padding 16px + font-size 12px + color #aaa + background #fff + border solid 1px rgba(#000, 0.075) + border-radius 6px + + a + color #999 + + i + color #ccc + +.xygkxeaeontfaokvqmiblezmhvhostak[data-darkmode] + root(true) + +.xygkxeaeontfaokvqmiblezmhvhostak:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue index 029e44e27..cac4007b4 100644 --- a/src/client/app/desktop/views/pages/welcome.vue +++ b/src/client/app/desktop/views/pages/welcome.vue @@ -7,6 +7,13 @@ </button> <div class="body" :style="{ backgroundImage: `url('${ welcomeBgUrl }')` }"> <div class="container"> + <div class="info"> + <span>%i18n:common.misskey% <b>{{ host }}</b></span> + <span class="stats" v-if="stats"> + <span>%fa:user% {{ stats.originalUsersCount | number }}</span> + <span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span> + </span> + </div> <main> <div class="about"> <h1 v-if="name">{{ name }}</h1> @@ -19,12 +26,8 @@ <mk-signin/> </div> </main> - <div class="info"> - <span>%i18n:common.misskey% <b>{{ host }}</b></span> - <span class="stats" v-if="stats"> - <span>%fa:user% {{ stats.originalUsersCount | number }}</span> - <span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span> - </span> + <div class="hashtags"> + <router-link v-for="tag in tags" :key="tag" :to="`/tags/${ tag }`" :title="tag">#{{ tag }}</router-link> </div> <mk-nav class="nav"/> </div> @@ -32,7 +35,7 @@ <img src="assets/title.dark.svg" alt="Misskey"> </div> <div class="tl"> - <mk-welcome-timeline/> + <mk-welcome-timeline :max="20"/> </div> <modal name="signup" width="500px" height="auto" scrollable> <header :class="$style.signupFormHeader">%i18n:@signup%</header> @@ -54,13 +57,18 @@ export default Vue.extend({ host, name, description, - pointerInterval: null + pointerInterval: null, + tags: [] }; }, created() { (this as any).api('stats').then(stats => { this.stats = stats; }); + + (this as any).api('hashtags/trend').then(stats => { + this.tags = stats.map(x => x.tag); + }); }, mounted() { this.point(); @@ -161,6 +169,20 @@ root(isDark) $loginWidth = 340px $width = $aboutWidth + $loginWidth + > .info + margin 0 auto 16px auto + width $width + font-size 14px + color #fff + + > .stats + margin-left 16px + padding-left 16px + border-left solid 1px #fff + + > * + margin-right 16px + > main display flex margin auto @@ -199,24 +221,19 @@ root(isDark) > .login width $loginWidth padding 16px 32px 32px 32px - background #f5f5f5 + background isDark ? #2e3440 : #f5f5f5 - > .info + > .hashtags margin 16px auto - padding 12px width $width font-size 14px color #fff - background rgba(#000, 0.2) + background rgba(#000, 0.3) border-radius 8px - > .stats - margin-left 16px - padding-left 16px - border-left solid 1px #fff - - > * - margin-right 16px + > * + display inline-block + margin 14px > .nav display block diff --git a/src/client/app/mobile/api/dialog.ts b/src/client/app/mobile/api/dialog.ts index a2378767b..23f35b7aa 100644 --- a/src/client/app/mobile/api/dialog.ts +++ b/src/client/app/mobile/api/dialog.ts @@ -1,5 +1,18 @@ -export default function(opts) { +import OS from '../../mios'; +import Dialog from '../views/components/dialog.vue'; + +export default (os: OS) => opts => { return new Promise<string>((res, rej) => { - alert('dialog not implemented yet'); + const o = opts || {}; + const d = os.new(Dialog, { + title: o.title, + text: o.text, + modal: o.modal, + buttons: o.actions + }); + d.$once('clicked', id => { + res(id); + }); + document.body.appendChild(d.$el); }); -} +}; diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts index cc0a8331b..cb43f9d52 100644 --- a/src/client/app/mobile/script.ts +++ b/src/client/app/mobile/script.ts @@ -38,6 +38,7 @@ import MkSettings from './views/pages/settings.vue'; import MkReversi from './views/pages/reversi.vue'; import MkTag from './views/pages/tag.vue'; import MkShare from './views/pages/share.vue'; +import MkFollow from '../common/views/pages/follow.vue'; /** * init @@ -80,7 +81,8 @@ init((launch) => { { path: '/@:user', component: MkUser }, { path: '/@:user/followers', component: MkFollowers }, { path: '/@:user/following', component: MkFollowing }, - { path: '/notes/:note', component: MkNote } + { path: '/notes/:note', component: MkNote }, + { path: '/authorize-follow', component: MkFollow } ] }); @@ -88,7 +90,7 @@ init((launch) => { launch(router, os => ({ chooseDriveFolder, chooseDriveFile, - dialog, + dialog: dialog(os), input, post: post(os), notify diff --git a/src/client/app/mobile/views/components/dialog.vue b/src/client/app/mobile/views/components/dialog.vue new file mode 100644 index 000000000..9ee01cb78 --- /dev/null +++ b/src/client/app/mobile/views/components/dialog.vue @@ -0,0 +1,171 @@ +<template> +<div class="mk-dialog"> + <div class="bg" ref="bg" @click="onBgClick"></div> + <div class="main" ref="main"> + <header v-html="title" :class="$style.header"></header> + <div class="body" v-html="text"></div> + <div class="buttons"> + <button v-for="button in buttons" @click="click(button)">{{ button.text }}</button> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; + +export default Vue.extend({ + props: { + title: { + type: String, + required: false + }, + text: { + type: String, + required: true + }, + buttons: { + type: Array, + default: () => { + return [{ + text: 'OK' + }]; + } + }, + modal: { + type: Boolean, + default: false + } + }, + mounted() { + this.$nextTick(() => { + (this.$refs.bg as any).style.pointerEvents = 'auto'; + anime({ + targets: this.$refs.bg, + opacity: 1, + duration: 100, + easing: 'linear' + }); + + anime({ + targets: this.$refs.main, + opacity: 1, + scale: [1.2, 1], + duration: 300, + easing: [0, 0.5, 0.5, 1] + }); + }); + }, + methods: { + click(button) { + this.$emit('clicked', button.id); + this.close(); + }, + close() { + (this.$refs.bg as any).style.pointerEvents = 'none'; + anime({ + targets: this.$refs.bg, + opacity: 0, + duration: 300, + easing: 'linear' + }); + + (this.$refs.main as any).style.pointerEvents = 'none'; + anime({ + targets: this.$refs.main, + opacity: 0, + scale: 0.8, + duration: 300, + easing: [ 0.5, -0.5, 1, 0.5 ], + complete: () => this.$destroy() + }); + }, + onBgClick() { + if (!this.modal) { + this.close(); + } + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-dialog + > .bg + display block + position fixed + z-index 8192 + top 0 + left 0 + width 100% + height 100% + background rgba(#000, 0.7) + opacity 0 + pointer-events none + + > .main + display block + position fixed + z-index 8192 + top 20% + left 0 + right 0 + margin 0 auto 0 auto + padding 16px + width calc(100% - 32px) + max-width 300px + background #fff + opacity 0 + + > .body + margin 1em 0 + color #888 + + > .buttons + > button + display inline-block + float right + margin 0 + padding 0 10px + font-size 1.1em + font-weight normal + text-decoration none + color #888 + background transparent + outline none + border none + border-radius 0 + cursor pointer + transition color 0.1s ease + + i + margin 0 0.375em + + &:hover + color $theme-color + + &:active + color darken($theme-color, 10%) + transition color 0s ease + +</style> + +<style lang="stylus" module> +@import '~const.styl' + +.header + margin 0 0 1em 0 + color $theme-color + // color #43A4EC + font-weight bold + + &:empty + display none + + > i + margin-right 0.5em + +</style> diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue index f3e77d706..fa1592218 100644 --- a/src/client/app/mobile/views/components/note-detail.vue +++ b/src/client/app/mobile/views/components/note-detail.vue @@ -38,10 +38,7 @@ <div class="text"> <span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span> <span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span> - <mk-note-html v-if="p.text" :text="p.text" :i="$store.state.i"/> - </div> - <div class="tags" v-if="p.tags && p.tags.length > 0"> - <router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link> + <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/> </div> <div class="media" v-if="p.media.length > 0"> <mk-media-list :media-list="p.media" :raw="true"/> @@ -83,7 +80,7 @@ <script lang="ts"> import Vue from 'vue'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; import MkNoteMenu from '../../../common/views/components/note-menu.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; @@ -197,7 +194,8 @@ export default Vue.extend({ (this as any).os.new(MkReactionPicker, { source: this.$refs.reactButton, note: this.p, - compact: true + compact: true, + big: true }); }, menu() { @@ -369,31 +367,6 @@ root(isDark) display block max-width 100% - > .tags - margin 4px 0 0 0 - - > * - display inline-block - margin 0 8px 0 0 - padding 2px 8px 2px 16px - font-size 90% - color #8d969e - background #edf0f3 - border-radius 4px - - &:before - content "" - display block - position absolute - top 0 - bottom 0 - left 4px - width 8px - height 8px - margin auto 0 - background #fff - border-radius 100% - > .time font-size 16px color isDark ? #606984 : #c0c0c0 diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue index 4498bb563..8fc8af7f8 100644 --- a/src/client/app/mobile/views/components/note.vue +++ b/src/client/app/mobile/views/components/note.vue @@ -25,16 +25,13 @@ <span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span> <span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span> <a class="reply" v-if="p.reply">%fa:reply%</a> - <mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="$store.state.i" :class="$style.text"/> + <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/> <a class="rp" v-if="p.renote != null">RP:</a> </div> <div class="media" v-if="p.media.length > 0"> <mk-media-list :media-list="p.media"/> </div> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> - <div class="tags" v-if="p.tags && p.tags.length > 0"> - <router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link> - </div> <mk-url-preview v-for="url in urls" :url="url" :key="url"/> <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> @@ -68,8 +65,7 @@ <script lang="ts"> import Vue from 'vue'; -import parse from '../../../../../text/parse'; -import canHideText from '../../../common/scripts/can-hide-text'; +import parse from '../../../../../mfm/parse'; import MkNoteMenu from '../../../common/views/components/note-menu.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; @@ -165,8 +161,6 @@ export default Vue.extend({ }, methods: { - canHideText, - capture(withHandler = false) { if (this.$store.getters.isSignedIn) { this.connection.send({ @@ -216,7 +210,8 @@ export default Vue.extend({ (this as any).os.new(MkReactionPicker, { source: this.$refs.reactButton, note: this.p, - compact: true + compact: true, + big: true }); }, @@ -419,31 +414,6 @@ root(isDark) .mk-url-preview margin-top 8px - > .tags - margin 4px 0 0 0 - - > * - display inline-block - margin 0 8px 0 0 - padding 2px 8px 2px 16px - font-size 90% - color #8d969e - background isDark ? #313543 : #edf0f3 - border-radius 4px - - &:before - content "" - display block - position absolute - top 0 - bottom 0 - left 4px - width 8px - height 8px - margin auto 0 - background isDark ? #282c37 : #fff - border-radius 100% - > .media > img display block diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue index 7aaf0424c..06d22c725 100644 --- a/src/client/app/mobile/views/components/notes.vue +++ b/src/client/app/mobile/views/components/notes.vue @@ -13,7 +13,9 @@ <button @click="resolveInitPromise">%i18n:@retry%</button> </div> - <transition-group name="mk-notes" class="transition"> + <!-- トランジションを有効にするとなぜかメモリリークする --> + <!-- <transition-group name="mk-notes" class="transition"> --> + <div class="transition"> <template v-for="(note, i) in _notes"> <mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> @@ -21,7 +23,8 @@ <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> </p> </template> - </transition-group> + </div> + <!-- </transition-group> --> <footer v-if="more"> <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue index 6bb9e9bb2..fc220c252 100644 --- a/src/client/app/mobile/views/components/notifications.vue +++ b/src/client/app/mobile/views/components/notifications.vue @@ -1,6 +1,8 @@ <template> <div class="mk-notifications"> - <transition-group name="mk-notifications" class="transition notifications"> + <!-- トランジションを有効にするとなぜかメモリリークする --> + <!-- <transition-group name="mk-notifications" class="transition notifications"> --> + <div class="transition notifications"> <template v-for="(notification, i) in _notifications"> <mk-notification :notification="notification" :key="notification.id"/> <p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date"> @@ -8,7 +10,8 @@ <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> </p> </template> - </transition-group> + </div> + <!-- </transition-group> --> <button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template> diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue index 62fa18508..1015a4411 100644 --- a/src/client/app/mobile/views/components/post-form.vue +++ b/src/client/app/mobile/views/components/post-form.vue @@ -45,7 +45,7 @@ import Vue from 'vue'; import * as XDraggable from 'vuedraggable'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import getKao from '../../../common/scripts/get-kao'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; import { host } from '../../../config'; export default Vue.extend({ diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue index 4ad90b97d..a4ce49786 100644 --- a/src/client/app/mobile/views/components/sub-note-content.vue +++ b/src/client/app/mobile/views/components/sub-note-content.vue @@ -4,7 +4,7 @@ <span v-if="note.isHidden" style="opacity: 0.5">(%i18n:@private%)</span> <span v-if="note.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span> <a class="reply" v-if="note.replyId">%fa:reply%</a> - <mk-note-html v-if="note.text" :text="note.text" :i="$store.state.i"/> + <misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/> <a class="rp" v-if="note.renoteId">RP: ...</a> </div> <details v-if="note.media.length > 0"> diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index 34482fccb..89e5eaff6 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -13,6 +13,7 @@ <ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch> <ui-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</ui-switch> <ui-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi">%i18n:common.i-like-sushi%</ui-switch> + <ui-switch v-model="$store.state.settings.reversiBoardLabels" @change="onChangeReversiBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch> <div> <div>%i18n:@timeline%</div> @@ -182,6 +183,13 @@ export default Vue.extend({ }); }, + onChangeReversiBoardLabels(v) { + this.$store.dispatch('settings/set', { + key: 'reversiBoardLabels', + value: v + }); + }, + onChangeShowReplyTarget(v) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index 3d3701590..ba9de9f8a 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -18,7 +18,9 @@ <span class="username"><mk-acct :user="user"/></span> <span class="followed" v-if="user.isFollowed">%i18n:@follows-you%</span> </div> - <div class="description">{{ user.description }}</div> + <div class="description"> + <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/> + </div> <div class="info"> <p class="location" v-if="user.host === null && user.profile.location"> %fa:map-marker%{{ user.profile.location }} diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue index cd8f5841e..ec21588ad 100644 --- a/src/client/app/mobile/views/pages/welcome.vue +++ b/src/client/app/mobile/views/pages/welcome.vue @@ -14,6 +14,9 @@ <div class="tl"> <mk-welcome-timeline/> </div> + <div class="hashtags"> + <router-link v-for="tag in tags" :key="tag" :to="`/tags/${ tag }`" :title="tag">#{{ tag }}</router-link> + </div> <div class="stats" v-if="stats"> <span>%fa:user% {{ stats.originalUsersCount | number }}</span> <span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span> @@ -37,13 +40,18 @@ export default Vue.extend({ stats: null, host, name, - description + description, + tags: [] }; }, created() { (this as any).api('stats').then(stats => { this.stats = stats; }); + + (this as any).api('hashtags/trend').then(stats => { + this.tags = stats.map(x => x.tag); + }); } }); </script> @@ -116,12 +124,22 @@ export default Vue.extend({ box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2) > .tl + margin 16px 0 + > * max-height 300px border-radius 6px overflow auto -webkit-overflow-scrolling touch + > .hashtags + padding 16px 0 + border solid 2px #ddd + border-radius 8px + + > * + margin 0 16px + > .stats margin 16px 0 padding 8px diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 1bc39ae66..dfb24bb5f 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -19,7 +19,8 @@ const defaultSettings = { loadRemoteMedia: true, disableViaMobile: false, memo: null, - iLikeSushi: false + iLikeSushi: false, + reversiBoardLabels: false }; const defaultDeviceSettings = { diff --git a/src/client/assets/manifest.json b/src/client/assets/manifest.json index dcd1e2679..bae0ee7f1 100644 --- a/src/client/assets/manifest.json +++ b/src/client/assets/manifest.json @@ -37,6 +37,6 @@ } ], "share_target": { - "url_template": "share?text={title}%20-%20{text}%20-%20{url}" + "url_template": "share?text=【{title}】%0A{text}%0A{url}" } } diff --git a/src/mfm/html-to-mfm.ts b/src/mfm/html-to-mfm.ts new file mode 100644 index 000000000..540635036 --- /dev/null +++ b/src/mfm/html-to-mfm.ts @@ -0,0 +1,71 @@ +const parse5 = require('parse5'); + +export default function(html: string): string { + const dom = parse5.parseFragment(html); + + let text = ''; + + dom.childNodes.forEach((n: any) => analyze(n)); + + return text.trim(); + + function getText(node: any) { + if (node.nodeName == '#text') return node.value; + + if (node.childNodes) { + return node.childNodes.map((n: any) => getText(n)).join(''); + } + + return ''; + } + + function analyze(node: any) { + switch (node.nodeName) { + case '#text': + text += node.value; + break; + + case 'br': + text += '\n'; + break; + + case 'a': + const txt = getText(node); + + // メンション + if (txt.startsWith('@')) { + const part = txt.split('@'); + + if (part.length == 2) { + //#region ホスト名部分が省略されているので復元する + const href = new URL(node.attrs.find((x: any) => x.name == 'href').value); + const acct = txt + '@' + href.hostname; + text += acct; + break; + //#endregion + } else if (part.length == 3) { + text += txt; + break; + } + } + + if (node.childNodes) { + node.childNodes.forEach((n: any) => analyze(n)); + } + break; + + case 'p': + text += '\n\n'; + if (node.childNodes) { + node.childNodes.forEach((n: any) => analyze(n)); + } + break; + + default: + if (node.childNodes) { + node.childNodes.forEach((n: any) => analyze(n)); + } + break; + } + } +} diff --git a/src/text/html.ts b/src/mfm/html.ts similarity index 100% rename from src/text/html.ts rename to src/mfm/html.ts diff --git a/src/text/parse/core/syntax-highlighter.ts b/src/mfm/parse/core/syntax-highlighter.ts similarity index 100% rename from src/text/parse/core/syntax-highlighter.ts rename to src/mfm/parse/core/syntax-highlighter.ts diff --git a/src/text/parse/elements/bold.ts b/src/mfm/parse/elements/bold.ts similarity index 100% rename from src/text/parse/elements/bold.ts rename to src/mfm/parse/elements/bold.ts diff --git a/src/text/parse/elements/code.ts b/src/mfm/parse/elements/code.ts similarity index 100% rename from src/text/parse/elements/code.ts rename to src/mfm/parse/elements/code.ts diff --git a/src/text/parse/elements/emoji.ts b/src/mfm/parse/elements/emoji.ts similarity index 100% rename from src/text/parse/elements/emoji.ts rename to src/mfm/parse/elements/emoji.ts diff --git a/src/text/parse/elements/hashtag.ts b/src/mfm/parse/elements/hashtag.ts similarity index 100% rename from src/text/parse/elements/hashtag.ts rename to src/mfm/parse/elements/hashtag.ts diff --git a/src/text/parse/elements/inline-code.ts b/src/mfm/parse/elements/inline-code.ts similarity index 100% rename from src/text/parse/elements/inline-code.ts rename to src/mfm/parse/elements/inline-code.ts diff --git a/src/text/parse/elements/link.ts b/src/mfm/parse/elements/link.ts similarity index 100% rename from src/text/parse/elements/link.ts rename to src/mfm/parse/elements/link.ts diff --git a/src/text/parse/elements/mention.ts b/src/mfm/parse/elements/mention.ts similarity index 100% rename from src/text/parse/elements/mention.ts rename to src/mfm/parse/elements/mention.ts diff --git a/src/text/parse/elements/quote.ts b/src/mfm/parse/elements/quote.ts similarity index 100% rename from src/text/parse/elements/quote.ts rename to src/mfm/parse/elements/quote.ts diff --git a/src/text/parse/elements/search.ts b/src/mfm/parse/elements/search.ts similarity index 80% rename from src/text/parse/elements/search.ts rename to src/mfm/parse/elements/search.ts index e5d9b9f0c..9c4b7ffbe 100644 --- a/src/text/parse/elements/search.ts +++ b/src/mfm/parse/elements/search.ts @@ -9,7 +9,7 @@ export type TextElementSearch = { }; export default function(text: string) { - const match = text.match(/^(.+?) 検索(\n|$)/); + const match = text.match(/^(.+?) (検索|Search)(\n|$)/i); if (!match) return null; return { type: 'search', diff --git a/src/text/parse/elements/title.ts b/src/mfm/parse/elements/title.ts similarity index 85% rename from src/text/parse/elements/title.ts rename to src/mfm/parse/elements/title.ts index b89739a7c..86ce8ab47 100644 --- a/src/text/parse/elements/title.ts +++ b/src/mfm/parse/elements/title.ts @@ -9,7 +9,7 @@ export type TextElementTitle = { }; export default function(text: string) { - const match = text.match(/^【(.+?)】\n/); + const match = text.match(/^(【|\[)(.+?)(】|])\n/); if (!match) return null; const title = match[0]; return { diff --git a/src/text/parse/elements/url.ts b/src/mfm/parse/elements/url.ts similarity index 100% rename from src/text/parse/elements/url.ts rename to src/mfm/parse/elements/url.ts diff --git a/src/text/parse/index.ts b/src/mfm/parse/index.ts similarity index 100% rename from src/text/parse/index.ts rename to src/mfm/parse/index.ts diff --git a/src/publishers/notify.ts b/src/publishers/notify.ts index 0e480ef01..5b25fbf8a 100644 --- a/src/publishers/notify.ts +++ b/src/publishers/notify.ts @@ -4,6 +4,7 @@ import Mute from '../models/mute'; import { pack } from '../models/notification'; import stream from './stream'; import User from '../models/user'; +import pushSw from '../publishers/push-sw'; export default ( notifiee: mongo.ObjectID, @@ -26,9 +27,10 @@ export default ( resolve(notification); + const packed = await pack(notification); + // Publish notification event - stream(notifiee, 'notification', - await pack(notification)); + stream(notifiee, 'notification', packed); // Update flag User.update({ _id: notifiee }, { @@ -52,7 +54,9 @@ export default ( } //#endregion - stream(notifiee, 'unread_notification', await pack(notification)); + stream(notifiee, 'unread_notification', packed); + + pushSw(notifiee, 'notification', packed); } }, 3000); }); diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts index 0ceecdd00..8df440930 100644 --- a/src/remote/activitypub/misc/get-note-html.ts +++ b/src/remote/activitypub/misc/get-note-html.ts @@ -1,6 +1,6 @@ import { INote } from '../../../models/note'; -import toHtml from '../../../text/html'; -import parse from '../../../text/parse'; +import toHtml from '../../../mfm/html'; +import parse from '../../../mfm/parse'; import config from '../../../config'; export default function(note: INote) { diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index b0fe045e6..85a8f89bc 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -1,5 +1,4 @@ import * as mongo from 'mongodb'; -const parse5 = require('parse5'); import * as debug from 'debug'; import config from '../../../config'; @@ -10,79 +9,10 @@ import { INote as INoteActivityStreamsObject, IObject } from '../type'; import { resolvePerson, updatePerson } from './person'; import { resolveImage } from './image'; import { IRemoteUser, IUser } from '../../../models/user'; +import htmlToMFM from '../../../mfm/html-to-mfm'; const log = debug('misskey:activitypub'); -function parse(html: string): string { - const dom = parse5.parseFragment(html); - - let text = ''; - - dom.childNodes.forEach((n: any) => analyze(n)); - - return text.trim(); - - function getText(node: any) { - if (node.nodeName == '#text') return node.value; - - if (node.childNodes) { - return node.childNodes.map((n: any) => getText(n)).join(''); - } - - return ''; - } - - function analyze(node: any) { - switch (node.nodeName) { - case '#text': - text += node.value; - break; - - case 'br': - text += '\n'; - break; - - case 'a': - const txt = getText(node); - - // メンション - if (txt.startsWith('@')) { - const part = txt.split('@'); - - if (part.length == 2) { - //#region ホスト名部分が省略されているので復元する - const href = new URL(node.attrs.find((x: any) => x.name == 'href').value); - const acct = txt + '@' + href.hostname; - text += acct; - break; - //#endregion - } else if (part.length == 3) { - text += txt; - break; - } - } - - if (node.childNodes) { - node.childNodes.forEach((n: any) => analyze(n)); - } - break; - - case 'p': - text += '\n\n'; - if (node.childNodes) { - node.childNodes.forEach((n: any) => analyze(n)); - } - break; - - default: - if (node.childNodes) { - node.childNodes.forEach((n: any) => analyze(n)); - } - break; - } - } -} - /** * Noteをフェッチします。 * @@ -158,7 +88,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null; // テキストのパース - const text = parse(note.content); + const text = htmlToMFM(note.content); // ユーザーの情報が古かったらついでに更新しておく if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) { diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index 42ee5a27d..94a6cee0b 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -1,5 +1,4 @@ import * as mongo from 'mongodb'; -import { JSDOM } from 'jsdom'; import { toUnicode } from 'punycode'; import * as debug from 'debug'; @@ -11,6 +10,7 @@ import { resolveImage } from './image'; import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type'; import { IDriveFile } from '../../../models/drive-file'; import Meta from '../../../models/meta'; +import htmlToMFM from '../../../mfm/html-to-mfm'; const log = debug('misskey:activitypub'); @@ -47,16 +47,28 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs const object = await resolver.resolve(value) as any; - if ( - object == null || - object.type !== 'Person' || - typeof object.preferredUsername !== 'string' || - typeof object.inbox !== 'string' || - !validateUsername(object.preferredUsername) || - !isValidName(object.name == '' ? null : object.name) - ) { - log(`invalid person: ${JSON.stringify(object, null, 2)}`); - throw new Error('invalid person'); + if (object == null) { + throw new Error('invalid person: object is null'); + } + + if (object.type != 'Person' && object.type != 'Service') { + throw new Error('invalid person: object is not a person or service'); + } + + if (typeof object.preferredUsername !== 'string') { + throw new Error('invalid person: preferredUsername is not a string'); + } + + if (typeof object.inbox !== 'string') { + throw new Error('invalid person: inbox is not a string'); + } + + if (!validateUsername(object.preferredUsername)) { + throw new Error('invalid person: invalid username'); + } + + if (!isValidName(object.name == '' ? null : object.name)) { + throw new Error('invalid person: invalid name'); } const person: IPerson = object; @@ -80,7 +92,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs ]); const host = toUnicode(finger.subject.replace(/^.*?@/, '')).toLowerCase(); - const summaryDOM = JSDOM.fragment(person.summary); + + const isBot = object.type == 'Service'; // Create user let user: IRemoteUser; @@ -89,7 +102,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs avatarId: null, bannerId: null, createdAt: Date.parse(person.published) || null, - description: summaryDOM.textContent, + description: htmlToMFM(person.summary), followersCount, followingCount, notesCount, @@ -106,7 +119,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs inbox: person.inbox, endpoints: person.endpoints, uri: person.id, - url: person.url + url: person.url, + isBot }) as IRemoteUser; } catch (e) { // duplicate key error @@ -211,8 +225,6 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver) ) ]); - const summaryDOM = JSDOM.fragment(person.summary); - // アイコンとヘッダー画像をフェッチ const [avatar, banner] = (await Promise.all<IDriveFile>([ person.icon, @@ -231,7 +243,7 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver) bannerId: banner ? banner._id : null, avatarUrl: avatar && avatar.metadata.isMetaOnly ? avatar.metadata.url : null, bannerUrl: banner && banner.metadata.isMetaOnly ? banner.metadata.url : null, - description: summaryDOM.textContent, + description: htmlToMFM(person.summary), followersCount, followingCount, notesCount, diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts index b908f8bb1..7cee2be22 100644 --- a/src/remote/activitypub/renderer/note.ts +++ b/src/remote/activitypub/renderer/note.ts @@ -54,11 +54,11 @@ export default async function renderNote(note: INote, dive = true): Promise<any> ? [`${attributedTo}/followers`].concat(mentions) : []; - const mentionedUsers = await User.find({ + const mentionedUsers = note.mentions ? await User.find({ _id: { $in: note.mentions } - }); + }) : []; const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag)); const mentionTags = mentionedUsers.map(u => renderMention(u)); diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts index 8825c56c2..d4b3f40e4 100644 --- a/src/remote/activitypub/renderer/person.ts +++ b/src/remote/activitypub/renderer/person.ts @@ -2,12 +2,14 @@ import renderImage from './image'; import renderKey from './key'; import config from '../../../config'; import { ILocalUser } from '../../../models/user'; +import toHtml from '../../../mfm/html'; +import parse from '../../../mfm/parse'; export default (user: ILocalUser) => { const id = `${config.url}/users/${user._id}`; return { - type: 'Person', + type: user.isBot ? 'Service' : 'Person', id, inbox: `${id}/inbox`, outbox: `${id}/outbox`, @@ -15,7 +17,7 @@ export default (user: ILocalUser) => { url: `${config.url}/@${user.username}`, preferredUsername: user.username, name: user.name, - summary: user.description, + summary: toHtml(parse(user.description)), icon: user.avatarId && renderImage(user.avatarId), image: user.bannerId && renderImage(user.bannerId), manuallyApprovesFollowers: user.isLocked, diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts index 1fbc621e9..f8a01a6ff 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -11,7 +11,7 @@ import renderNote from '../remote/activitypub/renderer/note'; import renderKey from '../remote/activitypub/renderer/key'; import renderPerson from '../remote/activitypub/renderer/person'; import renderOrderedCollection from '../remote/activitypub/renderer/ordered-collection'; -//import parseAcct from '../acct/parse'; +import parseAcct from '../acct/parse'; import config from '../config'; // Init router @@ -142,20 +142,6 @@ router.get('/@:user', async (ctx, next) => { userInfo(ctx, user); }); - -// follow form -router.get('/authorize-follow', async ctx => { - /* TODO - const { username, host } = parseAcct(ctx.query.acct); - if (host === null) { - res.sendStatus(422); - return; - } - - const finger = await request(`https://${host}`) - */ -}); - //#endregion export default router; diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts index a5d13b023..f613710c8 100644 --- a/src/server/api/endpoints.ts +++ b/src/server/api/endpoints.ts @@ -629,8 +629,7 @@ const endpoints: Endpoint[] = [ }, { - name: 'hashtags/trend', - withCredential: true + name: 'hashtags/trend' }, { diff --git a/src/server/api/index.ts b/src/server/api/index.ts index c39911c35..004c21b82 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -45,7 +45,6 @@ router.post('/signin', require('./private/signin').default); router.use(require('./service/github').routes()); router.use(require('./service/twitter').routes()); -router.use(require('./bot/interfaces/line').routes()); // Register router app.use(router.routes()); diff --git a/src/services/note/create.ts b/src/services/note/create.ts index ef03c4e04..a793c8e58 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -12,67 +12,45 @@ import notify from '../../publishers/notify'; import NoteWatching from '../../models/note-watching'; import watch from './watch'; import Mute from '../../models/mute'; -import pushSw from '../../publishers/push-sw'; import event from '../../publishers/stream'; -import parse from '../../text/parse'; +import parse from '../../mfm/parse'; import { IApp } from '../../models/app'; import UserList from '../../models/user-list'; import resolveUser from '../../remote/resolve-user'; import Meta from '../../models/meta'; -type Reason = 'reply' | 'quote' | 'mention'; +type Type = 'reply' | 'renote' | 'quote' | 'mention'; /** - * ServiceWorkerへの通知を担当 + * 通知を担当 */ class NotificationManager { - private user: IUser; - private note: any; - private list: Array<{ - user: ILocalUser['_id'], - reason: Reason; - }> = []; + private notifier: IUser; + private note: INote; - constructor(user: IUser, note: any) { - this.user = user; + constructor(notifier: IUser, note: INote) { + this.notifier = notifier; this.note = note; } - public push(user: ILocalUser['_id'], reason: Reason) { + public async push(notifiee: ILocalUser['_id'], type: Type) { // 自分自身へは通知しない - if (this.user._id.equals(user)) return; + if (this.notifier._id.equals(notifiee)) return; - const exist = this.list.find(x => x.user.equals(user)); + // ミュート情報を取得 + const mentioneeMutes = await Mute.find({ + muterId: notifiee + }); - if (exist) { - // 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする - if (reason != 'mention') { - exist.reason = reason; - } - } else { - this.list.push({ - user, reason + const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString()); + + // 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する + if (!mentioneesMutedUserIds.includes(this.notifier._id.toString())) { + notify(notifiee, this.notifier._id, type, { + noteId: this.note._id }); } } - - public deliver() { - this.list.forEach(async x => { - const mentionee = x.user; - - // ミュート情報を取得 - const mentioneeMutes = await Mute.find({ - muterId: mentionee - }); - - const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString()); - - // 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する - if (!mentioneesMutedUserIds.includes(this.user._id.toString())) { - pushSw(mentionee, x.reason, this.note); - } - }); - } } export default async (user: IUser, data: { @@ -214,7 +192,7 @@ export default async (user: IUser, data: { // Serialize const noteObj = await pack(note); - const nm = new NotificationManager(user, noteObj); + const nm = new NotificationManager(user, note); const render = async () => { const content = data.renote && data.text == null @@ -264,10 +242,6 @@ export default async (user: IUser, data: { if (data.renote && data.renote.userId.equals(u._id)) return; // Create notification - notify(u._id, user._id, 'mention', { - noteId: note._id - }); - nm.push(u._id, 'mention'); }); @@ -371,11 +345,6 @@ export default async (user: IUser, data: { } }); - // (自分自身へのリプライでない限りは)通知を作成 - notify(data.reply.userId, user._id, 'reply', { - noteId: note._id - }); - // Fetch watchers NoteWatching.find({ noteId: data.reply._id, @@ -388,9 +357,7 @@ export default async (user: IUser, data: { } }).then(watchers => { watchers.forEach(watcher => { - notify(watcher.userId, user._id, 'reply', { - noteId: note._id - }); + nm.push(watcher.userId, 'reply'); }); }); @@ -399,6 +366,7 @@ export default async (user: IUser, data: { watch(user._id, data.reply); } + // (自分自身へのリプライでない限りは)通知を作成 nm.push(data.reply.userId, 'reply'); } @@ -406,9 +374,7 @@ export default async (user: IUser, data: { if (data.renote) { // Notify const type = data.text ? 'quote' : 'renote'; - notify(data.renote.userId, user._id, type, { - noteId: note._id - }); + nm.push(data.renote.userId, type); // Fetch watchers NoteWatching.find({ @@ -420,9 +386,7 @@ export default async (user: IUser, data: { } }).then(watchers => { watchers.forEach(watcher => { - notify(watcher.userId, user._id, type, { - noteId: note._id - }); + nm.push(watcher.userId, type); }); }); diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index 5b30bb5e1..b3235e94d 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -1,9 +1,8 @@ -import { IUser, pack as packUser, isLocalUser, isRemoteUser } from '../../../models/user'; -import Note, { INote, pack as packNote } from '../../../models/note'; +import { IUser, isLocalUser, isRemoteUser } from '../../../models/user'; +import Note, { INote } from '../../../models/note'; import NoteReaction from '../../../models/note-reaction'; import { publishNoteStream } from '../../../publishers/stream'; import notify from '../../../publishers/notify'; -import pushSw from '../../../publishers/push-sw'; import NoteWatching from '../../../models/note-watching'; import watch from '../watch'; import renderLike from '../../../remote/activitypub/renderer/like'; @@ -54,12 +53,6 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise }); } - pushSw(note.userId, 'reaction', { - user: await packUser(user, note.userId), - note: await packNote(note, note.userId), - reaction: reaction - }); - // Fetch watchers NoteWatching .find({ diff --git a/src/utils/type.ts b/src/utils/type.ts deleted file mode 100644 index ba6ea0be7..000000000 --- a/src/utils/type.ts +++ /dev/null @@ -1,3 +0,0 @@ -// https://github.com/Microsoft/TypeScript/issues/12215 -export type Diff<T extends string, U extends string> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T]; -export type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] }; diff --git a/test/text.ts b/test/mfm.ts similarity index 77% rename from test/text.ts rename to test/mfm.ts index a64999fc0..df0f0be04 100644 --- a/test/text.ts +++ b/test/mfm.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; -import analyze from '../src/text/parse'; -import syntaxhighlighter from '../src/text/parse/core/syntax-highlighter'; +import analyze from '../src/mfm/parse'; +import syntaxhighlighter from '../src/mfm/parse/core/syntax-highlighter'; describe('Text', () => { it('can be analyzed', () => { @@ -93,6 +93,40 @@ describe('Text', () => { assert.equal(tokens[0].type, 'inline-code'); assert.equal(tokens[0].content, '`var x = "Strawberry Pasta";`'); }); + + it('search', () => { + const tokens1 = analyze('a b c 検索'); + assert.deepEqual([ + { type: 'search', content: 'a b c 検索', query: 'a b c'} + ], tokens1); + + const tokens2 = analyze('a b c Search'); + assert.deepEqual([ + { type: 'search', content: 'a b c Search', query: 'a b c'} + ], tokens2); + + const tokens3 = analyze('a b c search'); + assert.deepEqual([ + { type: 'search', content: 'a b c search', query: 'a b c'} + ], tokens3); + + const tokens4 = analyze('a b c SEARCH'); + assert.deepEqual([ + { type: 'search', content: 'a b c SEARCH', query: 'a b c'} + ], tokens4); + }); + + it('title', () => { + const tokens1 = analyze('【yee】\nhaw'); + assert.deepEqual( + { type: 'title', content: '【yee】\n', title: 'yee'} + , tokens1[0]); + + const tokens2 = analyze('[yee]\nhaw'); + assert.deepEqual( + { type: 'title', content: '[yee]\n', title: 'yee'} + , tokens2[0]); + }); }); describe('syntax highlighting', () => {