diff --git a/.gitignore b/.gitignore index 51e6a31b7..2cea822c1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /node_modules /build /built +built /data /.cache-loader npm-debug.log diff --git a/locales/ja.yml b/locales/ja.yml index 4cbee2512..ecde1bb6a 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -11,6 +11,8 @@ common: warning: "<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。" application-authorization: "アプリの連携" close: "閉じる" + do-not-copy-paste: "ここにコードを入力したり張り付けたりしないでください。アカウントが不正利用される可能性があります。" + got-it: "わかった" customization-tips: title: "カスタマイズのヒント" paragraph1: "ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。" @@ -26,7 +28,6 @@ common: notified-by: "{}さんから" reply-from: "{}さんから返信:" quoted-by: "{}さんが引用:" - name: "Misskey" time: unknown: "なぞのじかん" future: "未来" @@ -38,16 +39,10 @@ common: weeks_ago: "{}週間前" months_ago: "{}ヶ月前" years_ago: "{}年前" + month-and-day: "{month}月 {day}日" trash: "ゴミ箱" - date: - full-year: "年" - month: "月" - day: "日" - hours: "時" - minutes: "分" - weekday-short: sunday: "日" monday: "月" @@ -95,6 +90,7 @@ common: i-like-sushi: "私は(プリンよりむしろ)寿司が好き" show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示" verified-user: "認証済みのユーザー" + disable-animated-mfm: "投稿内の動きのあるテキストを無効にする" reversi: drawn: "引き分け" @@ -182,6 +178,10 @@ common/views/components/games/reversi/reversi.vue: waiting-for: "{}を待っています" cancel: "キャンセル" +common/views/components/games/reversi/reversi.game.vue: + surrender: "投了" + surrendered: "投了により" + common/views/components/games/reversi/reversi.index.vue: title: "Misskey Reversi" sub-title: "他のMisskeyユーザーとリバーシで対戦しよう" @@ -209,7 +209,7 @@ common/views/components/games/reversi/reversi.room.vue: looped-map: "ループマップ" can-put-everywhere: "どこでも置けるモード" settings-of-the-bot: "Botの設定" - this-gane-is-started-soon: "ゲームは数秒後に開始されます" + this-game-is-started-soon: "ゲームは数秒後に開始されます" waiting-for-other: "相手の準備が完了するのを待っています" waiting-for-me: "あなたの準備が完了するのを待っています" waiting-for-both: "準備中" @@ -254,12 +254,14 @@ common/views/components/messaging-room.vue: no-history: "これより過去の履歴はありません" resize-form: "ドラッグしてフォームの広さを調整" new-message: "新しいメッセージがあります" + only-one-file-attached: "メッセージに添付できるのはひとつのファイルのみです" common/views/components/messaging-room.form.vue: input-message-here: "ここにメッセージを入力" send: "送信" attach-from-local: "PCからファイルを添付する" attach-from-drive: "ドライブからファイルを添付する" + only-one-file-attached: "メッセージに添付できるのはひとつのファイルのみです" common/views/components/messaging-room.message.vue: is-read: "既読" @@ -306,6 +308,8 @@ common/views/components/signin.vue: token: "トークン" signing-in: "やってます..." signin: "サインイン" + or: "または" + signin-with-twitter: "Twitterでログイン" common/views/components/signup.vue: username: "ユーザー名" @@ -433,6 +437,18 @@ common/views/pages/follow.vue: request-pending: "フォロー許可待ち" follow-request: "フォロー申請" +desktop: + banner-crop-title: "バナーとして表示する部分を選択" + banner: "バナー" + uploading-banner: "新しいバナーをアップロードしています" + banner-updated: "バナーを更新しました" + choose-banner: "バナーにする画像を選択" + avatar-crop-title: "アバターとして表示する部分を選択" + avatar: "アバター" + uploading-avatar: "新しいアバターをアップロードしています" + avatar-updated: "アバターを更新しました" + choose-avatar: "アバターにする画像を選択" + desktop/views/components/activity.chart.vue: total: "Black ... Total" notes: "Blue ... Notes" @@ -592,6 +608,8 @@ desktop/views/components/notes.note.vue: detail: "詳細" private: "この投稿は非公開です" deleted: "この投稿は削除されました" + hide: "隠す" + see-more: "もっと見る" desktop/views/components/notes.vue: error: "読み込みに失敗しました。" @@ -630,6 +648,7 @@ desktop/views/components/post-form.vue: geolocation-alert: "お使いの端末は位置情報に対応していません" error: "エラー" enter-username: "ユーザー名を入力してください" + annotations: "内容への注釈 (オプション)" desktop/views/components/post-form-window.vue: note: "新規投稿" @@ -818,6 +837,7 @@ desktop/views/components/timeline.vue: desktop/views/components/ui.header.vue: welcome-back: "おかえりなさい、" + adjective: "さん" desktop/views/components/ui.header.account.vue: profile: "プロフィール" @@ -850,11 +870,10 @@ desktop/views/components/received-follow-requests-window.vue: accept: "承認" reject: "拒否" - - desktop/views/components/user-lists-window.vue: title: "リスト" create-list: "リストを作成" + list-name: "リスト名" desktop/views/components/user-preview.vue: notes: "投稿" @@ -959,6 +978,8 @@ desktop/views/pages/user/user.profile.vue: mute: "ミュートする" muted: "ミュートしています" unmute: "ミュート解除" + push-to-a-list: "リストに追加" + list-pushed: "{user}を{list}に追加しました。" desktop/views/pages/user/user.header.vue: posts: "投稿" @@ -1114,6 +1135,7 @@ mobile/views/components/timeline.vue: mobile/views/components/ui.header.vue: welcome-back: "おかえりなさい、" + adjective: "さん" mobile/views/components/ui.nav.vue: timeline: "タイムライン" @@ -1175,6 +1197,8 @@ mobile/views/pages/welcome.vue: mobile/views/pages/widgets.vue: dashboard: "ダッシュボード" widgets-hints: "ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。" + add-widget: "追加" + customization-tips: "カスタマイズのヒント" mobile/views/pages/widgets/activity.vue: activity: "アクティビティ" @@ -1223,6 +1247,7 @@ mobile/views/pages/settings/settings.profile.vue: mobile/views/pages/search.vue: search: "検索" empty: "「{}」に関する投稿は見つかりませんでした。" + not-found: "「{}」に関する投稿は見つかりませんでした。" mobile/views/pages/selectdrive.vue: select-file: "ファイルを選択" @@ -1327,3 +1352,7 @@ docs: name: "名前" type: "型" description: "説明" + + +dev/views/index.vue: + manage-apps: "アプリの管理" diff --git a/package.json b/package.json index 54bf473e5..64f9b577a 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "misskey", "author": "syuilo <i@syuilo.com>", - "version": "5.10.0", - "clientVersion": "1.0.7828", + "version": "5.20.1", + "clientVersion": "1.0.8105", "codename": "nighthike", "main": "./built/index.js", "private": true, @@ -43,7 +43,6 @@ "@types/is-root": "1.0.0", "@types/is-url": "1.2.28", "@types/js-yaml": "3.11.2", - "@types/jsdom": "11.0.6", "@types/koa": "2.0.46", "@types/koa-bodyparser": "5.0.1", "@types/koa-compress": "2.0.8", @@ -60,8 +59,7 @@ "@types/mocha": "5.2.3", "@types/mongodb": "3.1.3", "@types/ms": "0.7.30", - "@types/node": "10.5.5", - "@types/parse5": "5.0.0", + "@types/node": "10.5.7", "@types/portscanner": "2.1.0", "@types/pug": "2.0.4", "@types/qrcode": "1.2.0", @@ -90,7 +88,7 @@ "bootstrap-vue": "2.0.0-rc.11", "cafy": "11.3.0", "chalk": "2.4.1", - "commander": "2.16.0", + "commander": "2.17.1", "crc-32": "1.2.0", "css-loader": "1.0.0", "dateformat": "3.0.3", @@ -100,7 +98,7 @@ "diskusage": "0.2.4", "dompurify": "1.0.5", "elasticsearch": "15.1.1", - "element-ui": "2.4.5", + "element-ui": "2.4.6", "emojilib": "2.3.0", "escape-regexp": "0.0.1", "eslint": "5.0.1", @@ -108,7 +106,7 @@ "eventemitter3": "3.1.0", "exif-js": "2.3.0", "file-loader": "1.1.11", - "file-type": "8.1.0", + "file-type": "9.0.0", "fuckadblock": "3.2.1", "gulp": "3.9.1", "gulp-cssnano": "2.1.3", @@ -156,7 +154,7 @@ "monk": "6.0.6", "ms": "2.1.1", "nan": "2.10.0", - "node-sass": "4.9.2", + "node-sass": "4.9.3", "node-sass-json-importer": "3.3.1", "nprogress": "0.2.0", "object-assign-deep": "0.4.0", @@ -185,11 +183,12 @@ "showdown-highlightjs-extension": "0.1.2", "single-line-log": "1.1.2", "speakeasy": "2.0.0", - "style-loader": "0.21.0", + "stringz": "1.0.0", + "style-loader": "0.22.1", "stylus": "0.54.5", "stylus-loader": "3.0.2", - "summaly": "2.0.6", - "systeminformation": "3.42.8", + "summaly": "2.1.2", + "systeminformation": "3.42.9", "syuilo-password-strength": "0.0.1", "textarea-caret": "3.1.0", "tmp": "0.0.33", @@ -197,7 +196,7 @@ "ts-node": "7.0.0", "tslint": "5.10.0", "typescript": "2.9.2", - "typescript-eslint-parser": "17.0.1", + "typescript-eslint-parser": "18.0.0", "uglify-es": "3.3.9", "url-loader": "1.0.1", "uuid": "3.3.2", @@ -206,7 +205,7 @@ "vue-cropperjs": "2.2.1", "vue-js-modal": "1.3.16", "vue-json-tree-view": "2.1.4", - "vue-loader": "15.2.6", + "vue-loader": "15.3.0", "vue-router": "3.0.1", "vue-style-loader": "4.1.1", "vue-template-compiler": "2.5.17", @@ -215,7 +214,7 @@ "vuex-persistedstate": "2.5.4", "web-push": "3.3.2", "webfinger.js": "2.6.6", - "webpack": "4.16.4", + "webpack": "4.16.5", "webpack-cli": "3.1.0", "websocket": "1.0.26", "ws": "6.0.0", diff --git a/src/client/app/auth/script.ts b/src/client/app/auth/script.ts index bdfdf70be..64ab6536d 100644 --- a/src/client/app/auth/script.ts +++ b/src/client/app/auth/script.ts @@ -8,14 +8,14 @@ import VueRouter from 'vue-router'; import './style.styl'; import init from '../init'; - import Index from './views/index.vue'; +import * as config from '../config'; /** * init */ init(launch => { - document.title = '%i18n:common.name% | %i18n:common.application-authorization%'; + document.title = `${config.name} | %i18n:common.application-authorization%`; // Init router const router = new VueRouter({ diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts index 4fb0610fd..f42af9437 100644 --- a/src/client/app/common/scripts/compose-notification.ts +++ b/src/client/app/common/scripts/compose-notification.ts @@ -38,7 +38,7 @@ export default function(type, data): Notification { switch (data.type) { case 'mention': return { - title: '%i18n:common.notification.notified-by%'.split("{}")[0] + `${getUserName(data.user)}さんから:` + '%i18n:common.notification.notified-by%'.split("{}")[1], + title: '%i18n:common.notification.notified-by%'.split("{}")[0] + `${getUserName(data.user)}:` + '%i18n:common.notification.notified-by%'.split("{}")[1], body: getNoteSummary(data), icon: data.user.avatarUrl }; diff --git a/src/client/app/common/scripts/date-stringify.ts b/src/client/app/common/scripts/date-stringify.ts deleted file mode 100644 index 2b8e52556..000000000 --- a/src/client/app/common/scripts/date-stringify.ts +++ /dev/null @@ -1,13 +0,0 @@ -export default date => { - if (typeof date == 'string') date = new Date(date); - return ( - date.getFullYear() + '%i18n:common.date.full-year%' + - (date.getMonth() + 1) + '%i18n:common.date.month%' + - date.getDate() + '%i18n:common.date.day%' + - ' ' + - date.getHours() + '%i18n:common.date.hours%' + - date.getMinutes() + '%i18n:common.date.minutes%' + - ' ' + - `(${['日', '月', '火', '水', '木', '金', '土'][date.getDay()]})` - ); -}; diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue index cd6066877..b274eaa0a 100644 --- a/src/client/app/common/views/components/autocomplete.vue +++ b/src/client/app/common/views/components/autocomplete.vue @@ -132,7 +132,7 @@ export default Vue.extend({ this.users = users; this.fetching = false; } else { - (this as any).api('users/search_by_username', { + (this as any).api('users/search', { query: this.q, limit: 30 }).then(users => { diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue index a924b62e6..6685296c1 100644 --- a/src/client/app/common/views/components/avatar.vue +++ b/src/client/app/common/views/components/avatar.vue @@ -1,6 +1,8 @@ <template> - <router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-if="disablePreview"></router-link> - <router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-else v-user-preview="user.id"></router-link> + <span class="mk-avatar" :title="user | acct" :style="style" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick"></span> + <span class="mk-avatar" :title="user | acct" :style="style" v-else-if="disableLink && disablePreview" @click="onClick"></span> + <router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id"></router-link> + <router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-else-if="!disableLink && disablePreview"></router-link> </template> <script lang="ts"> @@ -15,6 +17,10 @@ export default Vue.extend({ required: false, default: null }, + disableLink: { + required: false, + default: false + }, disablePreview: { required: false, default: false @@ -35,6 +41,11 @@ export default Vue.extend({ borderRadius: this.$store.state.settings.circleIcons ? '100%' : null }; } + }, + methods: { + onClick(e) { + this.$emit('click', e); + } } }); </script> diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue index bbfec2c1c..d1809d741 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.game.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue @@ -1,14 +1,18 @@ <template> <div class="xqnhankfuuilcwvhgsopeqncafzsquya"> - <header><b>{{ blackUser | userName }}</b>(%i18n:common.reversi.black%) vs <b>{{ whiteUser | userName }}</b>(%i18n:common.reversi.white%)</header> + <button class="go-index" v-if="selfNav" @click="goIndex">%fa:arrow-left%</button> + <header><b><router-link :to="blackUser | userPage">{{ blackUser | userName }}</router-link></b>(%i18n:common.reversi.black%) vs <b><router-link :to="whiteUser | userPage">{{ whiteUser | userName }}</router-link></b>(%i18n:common.reversi.white%)</header> - <div style="overflow: hidden"> + <div style="overflow: hidden; line-height: 28px;"> <p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ '%i18n:common.reversi.turn-of%'.replace('{}', $options.filters.userName(turnUser)) }}<mk-ellipsis/></p> <p class="turn" v-if="logPos != logs.length">{{ '%i18n:common.reversi.past-turn-of%'.replace('{}', $options.filters.userName(turnUser)) }}</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">{{ '%i18n:common.reversi.won%'.replace('{}', $options.filters.userName(game.winner)) }}{{ game.settings.isLlotheo ? ' (ロセオ)' : '' }}</template> + <template v-if="game.winner"> + <span>{{ '%i18n:common.reversi.won%'.replace('{}', $options.filters.userName(game.winner)) }}</span> + <span v-if="game.surrendered != null"> (%i18n:@surrendered%)</span> + </template> <template v-else>%i18n:common.reversi.drawn%</template> </p> </div> @@ -41,6 +45,10 @@ <p class="status"><b>{{ '%i18n:common.reversi.this-turn%'.split('{}')[0] }}{{ logPos }}{{ '%i18n:common.reversi.this-turn%'.split('{}')[1] }}</b> %i18n:common.reversi.black%:{{ o.blackCount }} %i18n:common.reversi.white%:{{ o.whiteCount }} %i18n:common.reversi.total%:{{ o.blackCount + o.whiteCount }}</p> + <div class="actions" v-if="!game.isEnded && iAmPlayer"> + <form-button @click="surrender">%i18n:@surrender%</form-button> + </div> + <div class="player" v-if="game.isEnded"> <el-button-group> <el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</el-button> @@ -62,7 +70,20 @@ import Reversi, { Color } from '../../../../../../../games/reversi/core'; import { url } from '../../../../../config'; export default Vue.extend({ - props: ['initGame', 'connection'], + props: { + initGame: { + type: Object, + require: true + }, + connection: { + type: Object, + require: true + }, + selfNav: { + type: Boolean, + require: true + } + }, data() { return { @@ -79,22 +100,27 @@ export default Vue.extend({ if (!this.$store.getters.isSignedIn) return false; return this.game.user1Id == this.$store.state.i.id || this.game.user2Id == this.$store.state.i.id; }, + myColor(): Color { if (!this.iAmPlayer) return null; if (this.game.user1Id == this.$store.state.i.id && this.game.black == 1) return true; if (this.game.user2Id == this.$store.state.i.id && this.game.black == 2) return true; return false; }, + opColor(): Color { if (!this.iAmPlayer) return null; return this.myColor === true ? false : true; }, + blackUser(): any { return this.game.black == 1 ? this.game.user1 : this.game.user2; }, + whiteUser(): any { return this.game.black == 1 ? this.game.user2 : this.game.user1; }, + turnUser(): any { if (this.o.turn === true) { return this.game.black == 1 ? this.game.user1 : this.game.user2; @@ -104,11 +130,13 @@ export default Vue.extend({ return null; } }, + isMyTurn(): boolean { if (!this.iAmPlayer) return false; if (this.turnUser == null) return false; return this.turnUser.id == this.$store.state.i.id; }, + cellsStyle(): any { return { 'grid-template-rows': `repeat(${this.game.settings.map.length}, 1fr)`, @@ -165,11 +193,13 @@ export default Vue.extend({ mounted() { this.connection.on('set', this.onSet); this.connection.on('rescue', this.onRescue); + this.connection.on('ended', this.onEnded); }, beforeDestroy() { this.connection.off('set', this.onSet); this.connection.off('rescue', this.onRescue); + this.connection.off('ended', this.onEnded); clearInterval(this.pollingClock); }, @@ -215,6 +245,10 @@ export default Vue.extend({ } }, + onEnded(x) { + this.game = x.game; + }, + checkEnd() { this.game.isEnded = this.o.isEnded; if (this.game.isEnded) { @@ -250,6 +284,16 @@ export default Vue.extend({ this.checkEnd(); this.$forceUpdate(); + }, + + surrender() { + (this as any).api('games/reversi/games/surrender', { + gameId: this.game.id + }); + }, + + goIndex() { + this.$emit('go-index'); } } }); @@ -261,10 +305,21 @@ export default Vue.extend({ root(isDark) text-align center + > .go-index + position absolute + top 0 + left 0 + z-index 1 + width 42px + height 42px + > header padding 8px border-bottom dashed 1px isDark ? #4c5761 : #c4cdd4 + a + color inherit + > .board width calc(100% - 16px) max-width 500px @@ -381,6 +436,9 @@ root(isDark) margin 0 padding 16px 0 + > .actions + padding-bottom 16px + > .player padding-bottom 32px diff --git a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue index 4969a9347..1539c88de 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue @@ -1,7 +1,7 @@ <template> <div> <x-room v-if="!g.isStarted" :game="g" :connection="connection"/> - <x-game v-else :init-game="g" :connection="connection"/> + <x-game v-else :init-game="g" :connection="connection" :self-nav="selfNav" @go-index="goIndex"/> </div> </template> @@ -16,7 +16,16 @@ export default Vue.extend({ XGame, XRoom }, - props: ['game'], + props: { + game: { + type: Object, + required: true + }, + selfNav: { + type: Boolean, + require: true + } + }, data() { return { connection: null, @@ -36,6 +45,9 @@ export default Vue.extend({ onStarted(game) { Object.assign(this.g, game); this.$forceUpdate(); + }, + goIndex() { + this.$emit('go-index'); } } }); diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue index 026159a0f..d4d35f6a8 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.index.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.index.vue @@ -96,11 +96,7 @@ export default Vue.extend({ methods: { go(game) { - (this as any).api('games/reversi/games/show', { - gameId: game.id - }).then(game => { - this.$emit('go', game); - }); + this.$emit('go', game); }, match() { diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue index de5040f63..aed8718dd 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.room.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.room.vue @@ -53,7 +53,7 @@ </div> </div> - <div class="card" v-if="form"> + <div class="card form" v-if="form"> <header> <span>%i18n:@settings-of-the-bot%</span> </header> @@ -65,7 +65,7 @@ :key="message.id"/> <template v-for="item in form"> - <mk-switch v-if="item.type == 'button'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm($event, item)">{{ item.desc || '' }}</mk-switch> + <mk-switch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm(item)">{{ item.desc || '' }}</mk-switch> <div class="card" v-if="item.type == 'radio'" :key="item.id"> <header> @@ -73,7 +73,17 @@ </header> <div> - <el-radio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :label="r.value" @change="onChangeForm($event, item)">{{ r.label }}</el-radio> + <form-radio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :value="r.value" @change="onChangeForm(item)">{{ r.label }}</form-radio> + </div> + </div> + + <div class="card" v-if="item.type == 'slider'" :key="item.id"> + <header> + <span>{{ item.label }}</span> + </header> + + <div> + <input type="range" :min="item.min" :max="item.max" :step="item.step || 1" v-model="item.value" @change="onChangeForm(item)"/> </div> </div> @@ -83,7 +93,7 @@ </header> <div> - <el-input v-model="item.value" @change="onChangeForm($event, item)"/> + <el-input v-model="item.value" @change="onChangeForm(item)"/> </div> </div> </template> @@ -93,7 +103,7 @@ <footer> <p class="status"> - <template v-if="isAccepted && isOpAccepted">%i18n:@this-gane-is-started-soon%<mk-ellipsis/></template> + <template v-if="isAccepted && isOpAccepted">%i18n:@this-game-is-started-soon%<mk-ellipsis/></template> <template v-if="isAccepted && !isOpAccepted">%i18n:@waiting-for-other%<mk-ellipsis/></template> <template v-if="!isAccepted && isOpAccepted">%i18n:@waiting-for-me%</template> <template v-if="!isAccepted && !isOpAccepted">%i18n:@waiting-for-both%<mk-ellipsis/></template> @@ -210,11 +220,11 @@ export default Vue.extend({ this.messages.unshift(x.message); }, - onChangeForm(v, item) { + onChangeForm(item) { this.connection.send({ type: 'update-form', id: item.id, - value: v + value: item.value }); }, @@ -274,6 +284,9 @@ root(isDark) color isDark ? #fff : #606266 cursor pointer transition border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) + -webkit-appearance none + -moz-appearance none + appearance none &:hover border-color isDark ? #a7aebd : #c0c4cc @@ -312,12 +325,20 @@ root(isDark) &[data-none] border-color transparent + &.form + > div + > .card + .card + margin-top 16px + + input[type='range'] + width 100% + .card max-width 400px border-radius 4px background isDark ? #282C37 : #fff color isDark ? #fff : #303133 - box-shadow 0 2px 12px 0 rgba(#000, 0.1) + box-shadow 0 2px 12px 0 rgba(#000, isDark ? 0.7 : 0.1) > header padding 18px 20px diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue index 94431845b..223ec4597 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.vue @@ -1,7 +1,7 @@ <template> <div class="vchtoekanapleubgzioubdtmlkribzfd"> <div v-if="game"> - <x-gameroom :game="game"/> + <x-gameroom :game="game" :self-nav="selfNav" @go-index="goIndex"/> </div> <div class="matching" v-else-if="matching"> <h1>{{ '%i18n:@matching.waiting-for%'.split('{}')[0] }}<b>{{ matching | userName }}</b>{{ '%i18n:@matching.waiting-for%'.split('{}')[1] }}<mk-ellipsis/></h1> @@ -9,6 +9,9 @@ <form-button round @click="cancel">%i18n:@matching.cancel%</form-button> </div> </div> + <div v-else-if="gameId"> + ... + </div> <div class="index" v-else> <x-index @go="nav" @matching="onMatching"/> </div> @@ -31,6 +34,11 @@ export default Vue.extend({ gameId: { type: String, required: false + }, + selfNav: { + type: Boolean, + require: false, + default: true } }, @@ -45,22 +53,18 @@ export default Vue.extend({ }, watch: { - gameId(id) { - if (id == null) { - this.game = null; - } else { - Progress.start(); - (this as any).api('games/reversi/games/show', { - gameId: id - }).then(game => { - this.nav(game, true); - Progress.done(); - }); - } + game() { + this.$emit('gamed', this.game); + }, + + gameId() { + this.fetch(); } }, mounted() { + this.fetch(); + if (this.$store.getters.isSignedIn) { this.connection = (this as any).os.streams.reversiStream.getConnection(); this.connectionId = (this as any).os.streams.reversiStream.use(); @@ -88,12 +92,32 @@ export default Vue.extend({ }, methods: { - nav(game, silent) { - this.matching = null; - this.game = game; + fetch() { + if (this.gameId == null) { + this.game = null; + } else { + Progress.start(); + (this as any).api('games/reversi/games/show', { + gameId: this.gameId + }).then(game => { + this.game = game; + Progress.done(); + }); + } + }, - if (!silent) { - this.$emit('nav', this.game); + async nav(game, actualNav = true) { + if (this.selfNav) { + // 受け取ったゲーム情報が省略されたものなら完全な情報を取得する + if (game != null && (game.settings == null || game.settings.map == null)) { + game = await (this as any).api('games/reversi/games/show', { + gameId: game.id + }); + } + + this.game = game; + } else { + this.$emit('nav', game, actualNav); } }, @@ -112,7 +136,8 @@ export default Vue.extend({ }).then(game => { if (game) { this.matching = null; - this.game = game; + + this.nav(game); } }); }, @@ -120,6 +145,11 @@ export default Vue.extend({ onMatched(game) { this.matching = null; this.game = game; + this.nav(game, false); + }, + + goIndex() { + this.nav(null); } } }); diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue index b6ca90266..f183749fa 100644 --- a/src/client/app/common/views/components/messaging-room.form.vue +++ b/src/client/app/common/views/components/messaging-room.form.vue @@ -83,7 +83,7 @@ export default Vue.extend({ } } else { if (items[0].kind == 'file') { - alert('メッセージに添付できるのはひとつのファイルのみです'); + alert('%i18n:only-one-file-attached%'); } } }, @@ -105,7 +105,7 @@ export default Vue.extend({ return; } else if (e.dataTransfer.files.length > 1) { e.preventDefault(); - alert('メッセージに添付できるのはひとつのファイルのみです'); + alert('%i18n:only-one-file-attached%'); return; } diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue index b2831d692..30143b4f1 100644 --- a/src/client/app/common/views/components/messaging-room.vue +++ b/src/client/app/common/views/components/messaging-room.vue @@ -61,7 +61,7 @@ export default Vue.extend({ const date = new Date(message.createdAt).getDate(); const month = new Date(message.createdAt).getMonth() + 1; message._date = date; - message._datetext = `${month}月 ${date}日`; + message._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString()); return message; }); }, @@ -111,7 +111,7 @@ export default Vue.extend({ this.form.upload(e.dataTransfer.files[0]); return; } else if (e.dataTransfer.files.length > 1) { - alert('メッセージに添付できるのはひとつのファイルのみです'); + alert('%i18n:@only-one-file-attached%'); return; } diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.ts b/src/client/app/common/views/components/misskey-flavored-markdown.ts index 1480c0325..e97da4302 100644 --- a/src/client/app/common/views/components/misskey-flavored-markdown.ts +++ b/src/client/app/common/views/components/misskey-flavored-markdown.ts @@ -1,5 +1,6 @@ import Vue from 'vue'; import * as emojilib from 'emojilib'; +import { length } from 'stringz'; import parse from '../../../../../mfm/parse'; import getAcct from '../../../../../misc/acct/render'; import { url } from '../../../config'; @@ -40,10 +41,13 @@ export default Vue.component('misskey-flavored-markdown', { ast = this.ast; } + let bigCount = 0; + let motionCount = 0; + // Parse ast to DOM const els = flatten(ast.map(token => { switch (token.type) { - case 'text': + case 'text': { const text = token.content.replace(/(\r\n|\n|\r)/g, '\n'); if (this.shouldBreak) { @@ -54,19 +58,52 @@ export default Vue.component('misskey-flavored-markdown', { } else { return createElement('span', text.replace(/\n/g, ' ')); } + } - case 'bold': - return createElement('strong', token.bold); + case 'bold': { + return createElement('b', token.bold); + } - case 'url': + case 'big': { + bigCount++; + const isLong = length(token.big) > 10; + const isMany = bigCount > 3; + return (createElement as any)('strong', { + attrs: { + style: `display: inline-block; font-size: ${ isMany ? '100%' : '150%' };` + }, + directives: [this.$store.state.settings.disableAnimatedMfm || isLong || isMany ? {} : { + name: 'animate-css', + value: { classes: 'tada', iteration: 'infinite' } + }] + }, token.big); + } + + case 'motion': { + motionCount++; + const isLong = length(token.motion) > 10; + const isMany = motionCount > 3; + return (createElement as any)('span', { + attrs: { + style: 'display: inline-block;' + }, + directives: [this.$store.state.settings.disableAnimatedMfm || isLong || isMany ? {} : { + name: 'animate-css', + value: { classes: 'rubberBand', iteration: 'infinite' } + }] + }, token.motion); + } + + case 'url': { return createElement(MkUrl, { props: { url: token.content, target: '_blank' } }); + } - case 'link': + case 'link': { return createElement('a', { attrs: { class: 'link', @@ -75,8 +112,9 @@ export default Vue.component('misskey-flavored-markdown', { title: token.url } }, token.title); + } - case 'mention': + case 'mention': { return (createElement as any)('a', { attrs: { href: `${url}/@${getAcct(token)}`, @@ -88,16 +126,18 @@ export default Vue.component('misskey-flavored-markdown', { value: token.content }] }, token.content); + } - case 'hashtag': + case 'hashtag': { return createElement('a', { attrs: { href: `${url}/tags/${encodeURIComponent(token.hashtag)}`, target: '_blank' } }, token.content); + } - case 'code': + case 'code': { return createElement('pre', { class: 'code' }, [ @@ -107,15 +147,17 @@ export default Vue.component('misskey-flavored-markdown', { } }) ]); + } - case 'inline-code': + case 'inline-code': { return createElement('code', { domProps: { innerHTML: token.html } }); + } - case 'quote': + case 'quote': { const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n'); if (this.shouldBreak) { @@ -134,27 +176,32 @@ export default Vue.component('misskey-flavored-markdown', { } }, text2.replace(/\n/g, ' ')); } + } - case 'title': + case 'title': { return createElement('div', { attrs: { class: 'title' } }, token.title); + } - case 'emoji': + case 'emoji': { const emoji = emojilib.lib[token.emoji]; return createElement('span', emoji ? emoji.char : token.content); + } - case 'search': + case 'search': { return createElement(MkGoogle, { props: { q: token.query } }); + } - default: + default: { console.log('unknown ast type:', token.type); + } } })); diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue index 58241cef0..deaeeca6a 100644 --- a/src/client/app/common/views/components/signin.vue +++ b/src/client/app/common/views/components/signin.vue @@ -12,7 +12,7 @@ </ui-input> <ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required/> <ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button> - <p style="margin: 8px 0;">または<a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a></p> + <p style="margin: 8px 0;">%i18n:@or%<a :href="`${apiUrl}/signin/twitter`">%i18n:@signin-with-twitter%</a></p> </form> </template> diff --git a/src/client/app/common/views/components/ui/form/button.vue b/src/client/app/common/views/components/ui/form/button.vue index 6e1475bc3..9c37b3118 100644 --- a/src/client/app/common/views/components/ui/form/button.vue +++ b/src/client/app/common/views/components/ui/form/button.vue @@ -45,6 +45,9 @@ root(isDark) color isDark ? #fff : #606266 transition 0.1s + * + pointer-events none + &:hover &:focus color $theme-color diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue index 1e625f69e..4f1e34c6c 100644 --- a/src/client/app/common/views/components/url-preview.vue +++ b/src/client/app/common/views/components/url-preview.vue @@ -2,6 +2,12 @@ <iframe v-if="youtubeId" type="text/html" height="250" :src="`https://www.youtube.com/embed/${youtubeId}?origin=${misskeyUrl}`" frameborder="0"/> +<iframe v-else-if="spotifyId" + :src="`https://open.spotify.com/embed/track/${spotifyId}`" + frameborder="0" allowtransparency="true" allow="encrypted-media" /> +<iframe v-else-if="nicovideoId" + :src="`https://embed.nicovideo.jp/watch/${nicovideoId}?oldScript=1&referer=${misskeyUrl}&from=${position || '0'}&allowProgrammaticFullScreen=1`" + frameborder="0" allow="autoplay; encrypted-media" allowfullscreen /> <div v-else-if="tweetUrl && detail" class="twitter"> <blockquote ref="tweet" class="twitter-tweet" :data-theme="$store.state.device.darkmode ? 'dark' : null"> <a :href="url"></a> @@ -49,6 +55,9 @@ export default Vue.extend({ icon: null, sitename: null, youtubeId: null, + spotifyId: null, + nicovideoId: null, + position: null, tweetUrl: null, misskeyUrl }; @@ -58,8 +67,20 @@ export default Vue.extend({ if (url.hostname == 'www.youtube.com') { this.youtubeId = url.searchParams.get('v'); + return; } else if (url.hostname == 'youtu.be') { this.youtubeId = url.pathname; + return; + } else if (url.hostname == 'open.spotify.com') { + this.spotifyId = url.pathname.split('/').reverse().filter(x => x !== '')[0]; + return; + } else if (['nicovideo.jp', 'www.nicovideo.jp', 'nico.ms'].includes(url.hostname)) { + const id = url.pathname.split('/').reverse().filter(x => x !== '')[0]; + if (['sm', 'nm', 'ax', 'ca', 'cd', 'cw', 'fx', 'ig', 'na', 'om', 'sd', 'sk', 'yk', 'yo', 'za', 'zb', 'zc', 'zd', 'ze', 'nl', 'so', ...Array(10).keys()].some(x => id.startsWith(x)) { + this.nicovideoId = id; + this.position = url.searchParams.get('from'); + return; + } } else if (this.detail && url.hostname == 'twitter.com' && /^\/.+\/status(es)?\/\d+/.test(url.pathname)) { this.tweetUrl = url; const twttr = (window as any).twttr || {}; @@ -79,19 +100,19 @@ export default Vue.extend({ twttr.ready = loadTweet; (window as any).twttr = twttr; } - } else { - fetch('/url?url=' + encodeURIComponent(this.url)).then(res => { - res.json().then(info => { - this.title = info.title; - this.description = info.description; - this.thumbnail = info.thumbnail; - this.icon = info.icon; - this.sitename = info.sitename; - - this.fetching = false; - }); - }); + return; } + fetch('/url?url=' + encodeURIComponent(this.url)).then(res => { + res.json().then(info => { + this.title = info.title; + this.description = info.description; + this.thumbnail = info.thumbnail; + this.icon = info.icon; + this.sitename = info.sitename; + + this.fetching = false; + }); + }); } }); </script> diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue index e1b5b1f12..13d855d20 100644 --- a/src/client/app/common/views/pages/follow.vue +++ b/src/client/app/common/views/pages/follow.vue @@ -71,7 +71,6 @@ export default Vue.extend({ this.user = user; this.fetching = false; Progress.done(); - document.title = getUserName(this.user) + ' | %i18n:common.name%'; }); }, diff --git a/src/client/app/common/views/widgets/server.disk.vue b/src/client/app/common/views/widgets/server.disk.vue index 5c7e9678d..99ce62405 100644 --- a/src/client/app/common/views/widgets/server.disk.vue +++ b/src/client/app/common/views/widgets/server.disk.vue @@ -4,7 +4,7 @@ <div> <p>%fa:R hdd%Storage</p> <p>Total: {{ total | bytes(1) }}</p> - <p>Available: {{ available | bytes(1) }}</p> + <p>Free: {{ available | bytes(1) }}</p> <p>Used: {{ used | bytes(1) }}</p> </div> </div> diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts index 887367a24..83820f92b 100644 --- a/src/client/app/desktop/api/update-avatar.ts +++ b/src/client/app/desktop/api/update-avatar.ts @@ -8,7 +8,7 @@ export default (os: OS) => (cb, file = null) => { const w = os.new(CropWindow, { image: file, - title: 'アバターとして表示する部分を選択', + title: '%i18n:desktop.avatar-crop-title%', aspectRatio: 1 / 1 }); @@ -18,11 +18,11 @@ export default (os: OS) => (cb, file = null) => { data.append('file', blob, file.name + '.cropped.png'); os.api('drive/folders/find', { - name: 'アイコン' + name: '%i18n:desktop.avatar%' }).then(iconFolder => { if (iconFolder.length === 0) { os.api('drive/folders/create', { - name: 'アイコン' + name: '%i18n:desktop.avatar%' }).then(iconFolder => { upload(data, iconFolder); }); @@ -41,7 +41,7 @@ export default (os: OS) => (cb, file = null) => { const upload = (data, folder) => { const dialog = os.new(ProgressDialog, { - title: '新しいアバターをアップロードしています' + title: '%i18n:desktop.uploading-avatar%' }); document.body.appendChild(dialog.$el); @@ -76,10 +76,10 @@ export default (os: OS) => (cb, file = null) => { }); os.apis.dialog({ - title: '%fa:info-circle%アバターを更新しました', - text: '新しいアバターが反映されるまで時間がかかる場合があります。', + title: '%fa:info-circle% %i18n:desktop.avatar-updated%', + text: null, actions: [{ - text: 'わかった' + text: '%i18n:common.got-it%' }] }); @@ -92,7 +92,7 @@ export default (os: OS) => (cb, file = null) => { } else { os.apis.chooseDriveFile({ multiple: false, - title: '%fa:image%アバターにする画像を選択' + title: '%fa:image% %i18n:desktop.choose-avatar%' }).then(file => { fileSelected(file); }); diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts index 4e6dd4e2c..33c4e306a 100644 --- a/src/client/app/desktop/api/update-banner.ts +++ b/src/client/app/desktop/api/update-banner.ts @@ -8,7 +8,7 @@ export default (os: OS) => { const cropImage = file => new Promise((resolve, reject) => { const w = os.new(CropWindow, { image: file, - title: 'バナーとして表示する部分を選択', + title: '%i18n:desktop.banner-crop-title%', aspectRatio: 16 / 9 }); @@ -18,11 +18,11 @@ export default (os: OS) => { data.append('file', blob, file.name + '.cropped.png'); os.api('drive/folders/find', { - name: 'バナー' + name: '%i18n:desktop.banner%' }).then(bannerFolder => { if (bannerFolder.length === 0) { os.api('drive/folders/create', { - name: 'バナー' + name: '%i18n:desktop.banner%' }).then(iconFolder => { resolve(upload(data, iconFolder)); }); @@ -43,7 +43,7 @@ export default (os: OS) => { const upload = (data, folder) => new Promise((resolve, reject) => { const dialog = os.new(ProgressDialog, { - title: '新しいバナーをアップロードしています' + title: '%i18n:desktop.uploading-banner%' }); document.body.appendChild(dialog.$el); @@ -79,10 +79,10 @@ export default (os: OS) => { }); os.apis.dialog({ - title: '%fa:info-circle%バナーを更新しました', - text: '新しいバナーが反映されるまで時間がかかる場合があります。', + title: '%fa:info-circle% %i18n:desktop.banner-updated%', + text: null, actions: [{ - text: 'わかった' + text: '%i18n:common.got-it%' }] }); @@ -95,7 +95,7 @@ export default (os: OS) => { ? Promise.resolve(file) : os.apis.chooseDriveFile({ multiple: false, - title: '%fa:image%バナーにする画像を選択' + title: '%fa:image% %i18n:desktop.choose-banner%' }); return selectedFile diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue index 36a588922..b6980fae7 100644 --- a/src/client/app/desktop/views/components/note-detail.vue +++ b/src/client/app/desktop/views/components/note-detail.vue @@ -79,7 +79,6 @@ <script lang="ts"> import Vue from 'vue'; -import dateStringify from '../../../common/scripts/date-stringify'; import parse from '../../../../../mfm/parse'; import MkPostFormWindow from './post-form-window.vue'; @@ -129,7 +128,7 @@ export default Vue.extend({ : 0; }, title(): string { - return dateStringify(this.p.createdAt); + return new Date(this.p.createdAt).toLocaleString(); }, urls(): string[] { if (this.p.text) { diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue index 2a4955724..c723db98c 100644 --- a/src/client/app/desktop/views/components/note-preview.vue +++ b/src/client/app/desktop/views/components/note-preview.vue @@ -12,7 +12,6 @@ <script lang="ts"> import Vue from 'vue'; -import dateStringify from '../../../common/scripts/date-stringify'; export default Vue.extend({ props: { @@ -28,7 +27,7 @@ export default Vue.extend({ }, computed: { title(): string { - return dateStringify(this.note.createdAt); + return new Date(this.note.createdAt).toLocaleString(); } } }); diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue index a8186fb7e..fc851e83e 100644 --- a/src/client/app/desktop/views/components/notes.note.sub.vue +++ b/src/client/app/desktop/views/components/notes.note.sub.vue @@ -12,13 +12,12 @@ <script lang="ts"> import Vue from 'vue'; -import dateStringify from '../../../common/scripts/date-stringify'; export default Vue.extend({ props: ['note'], computed: { title(): string { - return dateStringify(this.note.createdAt); + return new Date(this.note.createdAt).toLocaleString(); } } }); diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue index 3aa52f75f..a98df104a 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -18,7 +18,7 @@ <div class="body"> <p v-if="p.cw != null" class="cw"> <span class="text" v-if="p.cw != ''">{{ p.cw }}</span> - <span class="toggle" @click="showContent = !showContent">{{ showContent ? '隠す' : 'もっと見る' }}</span> + <span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@hide%' : '%i18n:@see-more%' }}</span> </p> <div class="content" v-show="p.cw == null || showContent"> <div class="text"> @@ -71,7 +71,6 @@ <script lang="ts"> import Vue from 'vue'; -import dateStringify from '../../../common/scripts/date-stringify'; import parse from '../../../../../mfm/parse'; import MkPostFormWindow from './post-form-window.vue'; @@ -128,7 +127,7 @@ export default Vue.extend({ }, title(): string { - return dateStringify(this.p.createdAt); + return new Date(this.p.createdAt).toLocaleString(); }, urls(): string[] { diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index 0ec2f16db..02167ef85 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -33,7 +33,7 @@ <script lang="ts"> import Vue from 'vue'; -import { url } from '../../../config'; +import * as config from '../../../config'; import getNoteSummary from '../../../../../misc/get-note-summary'; import XNote from './notes.note.vue'; @@ -69,7 +69,7 @@ export default Vue.extend({ const date = new Date(note.createdAt).getDate(); const month = new Date(note.createdAt).getMonth() + 1; note._date = date; - note._datetext = `${month}月 ${date}日`; + note._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString()); return note; }); } @@ -149,7 +149,7 @@ export default Vue.extend({ // サウンドを再生する if (this.$store.state.device.enableSounds && !silent) { - const sound = new Audio(`${url}/assets/post.mp3`); + const sound = new Audio(`${config.url}/assets/post.mp3`); sound.volume = this.$store.state.device.soundVolume; sound.play(); } @@ -187,7 +187,7 @@ export default Vue.extend({ clearNotification() { this.unreadCount = 0; - document.title = '%i18n:common.name%'; + document.title = config.name; }, onVisibilitychange() { diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue index b291e1f54..bfe71903e 100644 --- a/src/client/app/desktop/views/components/notifications.vue +++ b/src/client/app/desktop/views/components/notifications.vue @@ -130,7 +130,7 @@ export default Vue.extend({ const date = new Date(notification.createdAt).getDate(); const month = new Date(notification.createdAt).getMonth() + 1; notification._date = date; - notification._datetext = `${month}月 ${date}日`; + notification._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString()); return notification; }); } diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue index 9b8a0c0f1..334a45750 100644 --- a/src/client/app/desktop/views/components/post-form.vue +++ b/src/client/app/desktop/views/components/post-form.vue @@ -14,7 +14,7 @@ <b>%i18n:@recent-tags%:</b> <a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" title="%@click-to-tagging%">#{{ tag }}</a> </div> - <input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)"> + <input v-show="useCw" v-model="cw" placeholder="%i18n:@annotations%"> <textarea :class="{ with: (files.length != 0 || poll) }" ref="text" v-model="text" :disabled="posting" @keydown="onKeydown" @paste="onPaste" :placeholder="placeholder" diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index 00bd7a878..84ea768a5 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -55,6 +55,7 @@ <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%"/> + <mk-switch v-model="$store.state.settings.disableAnimatedMfm" @change="onChangeDisableAnimatedMfm" text="%i18n:common.disable-animated-mfm%"/> </section> <section class="web" v-show="page == 'web'"> @@ -376,6 +377,12 @@ export default Vue.extend({ value: v }); }, + onChangeDisableAnimatedMfm(v) { + this.$store.dispatch('settings/set', { + key: 'disableAnimatedMfm', + value: v + }); + }, onChangeGradientWindowHeader(v) { this.$store.dispatch('settings/set', { key: 'gradientWindowHeader', diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue index 1ed5c3523..83df4b2fb 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">%i18n:@welcome-back%<b>{{ $store.state.i | userName }}</b>さん</p> + <p ref="welcomeback" v-if="$store.getters.isSignedIn">%i18n:@welcome-back%<b>{{ $store.state.i | userName }}</b>%i18n:@adjective%</p> <div class="container" ref="mainContainer"> <div class="left"> <x-nav/> diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue index 47648c287..72ae9cf4e 100644 --- a/src/client/app/desktop/views/components/user-lists-window.vue +++ b/src/client/app/desktop/views/components/user-lists-window.vue @@ -27,7 +27,7 @@ export default Vue.extend({ methods: { add() { (this as any).apis.input({ - title: 'リスト名', + title: '%i18n:@list-name%', }).then(async title => { const list = await (this as any).api('users/lists/create', { title 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 a5ed45b64..3578e1728 100644 --- a/src/client/app/desktop/views/pages/deck/deck.notes.vue +++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue @@ -72,7 +72,7 @@ export default Vue.extend({ const date = new Date(note.createdAt).getDate(); const month = new Date(note.createdAt).getMonth() + 1; note._date = date; - note._datetext = `${month}月 ${date}日`; + note._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString()); return note; }); } 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 10c06b0ad..fcb74b914 100644 --- a/src/client/app/desktop/views/pages/deck/deck.notifications.vue +++ b/src/client/app/desktop/views/pages/deck/deck.notifications.vue @@ -51,7 +51,7 @@ export default Vue.extend({ const date = new Date(notification.createdAt).getDate(); const month = new Date(notification.createdAt).getMonth() + 1; notification._date = date; - notification._datetext = `${month}月 ${date}日`; + notification._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString()); return notification; }); } diff --git a/src/client/app/desktop/views/pages/games/reversi.vue b/src/client/app/desktop/views/pages/games/reversi.vue index 590bda2d8..ce9b42c65 100644 --- a/src/client/app/desktop/views/pages/games/reversi.vue +++ b/src/client/app/desktop/views/pages/games/reversi.vue @@ -1,6 +1,6 @@ <template> <component :is="ui ? 'mk-ui' : 'div'"> - <mk-reversi :game-id="$route.params.game" @nav="nav"/> + <mk-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/> </component> </template> @@ -14,9 +14,14 @@ export default Vue.extend({ } }, methods: { - nav(game) { - history.pushState(null, null, '/reversi/' + game.id); - }, + nav(game, actualNav) { + if (actualNav) { + this.$router.push('/reversi/' + game.id); + } else { + // TODO: https://github.com/vuejs/vue-router/issues/703 + this.$router.push('/reversi/' + game.id); + } + } } }); </script> diff --git a/src/client/app/desktop/views/pages/home-customize.vue b/src/client/app/desktop/views/pages/home-customize.vue index ffdcf39fe..4318e8982 100644 --- a/src/client/app/desktop/views/pages/home-customize.vue +++ b/src/client/app/desktop/views/pages/home-customize.vue @@ -4,9 +4,11 @@ <script lang="ts"> import Vue from 'vue'; +import * as config from '../../../config'; + export default Vue.extend({ mounted() { - document.title = '%i18n:common.name% - %i18n:@title%'; + document.title = `${config.name} - %i18n:@title%`; } }); </script> diff --git a/src/client/app/desktop/views/pages/home.vue b/src/client/app/desktop/views/pages/home.vue index 0c42e42bd..3d3c24bfa 100644 --- a/src/client/app/desktop/views/pages/home.vue +++ b/src/client/app/desktop/views/pages/home.vue @@ -7,6 +7,7 @@ <script lang="ts"> import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; +import * as config from '../../../config'; export default Vue.extend({ props: { @@ -16,7 +17,7 @@ export default Vue.extend({ } }, mounted() { - document.title = '%i18n:common.name%'; + document.title = config.name; Progress.start(); }, diff --git a/src/client/app/desktop/views/pages/share.vue b/src/client/app/desktop/views/pages/share.vue index f5f5c4e18..4a7bdb14c 100644 --- a/src/client/app/desktop/views/pages/share.vue +++ b/src/client/app/desktop/views/pages/share.vue @@ -1,6 +1,6 @@ <template> <div class="pptjhabgjtt7kwskbfv4y3uml6fpuhmr"> - <h1>{{'%i18n:@share-with%'.split("{}")[0] + '%i18n:common.name%' + '%i18n:@share-with%'.split("{}")[1]}}</h1> + <h1>{{ '%i18n:@share-with%'.replace('{}', name) }}</h1> <div> <mk-signin v-if="!$store.getters.isSignedIn"/> <mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/> @@ -12,10 +12,12 @@ <script lang="ts"> import Vue from 'vue'; +import * as config from '../../../config'; export default Vue.extend({ data() { return { + name: config.name, posted: false, text: new URLSearchParams(location.search).get('text') }; 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 b74dbc7e2..efd5be467 100644 --- a/src/client/app/desktop/views/pages/user/user.profile.vue +++ b/src/client/app/desktop/views/pages/user/user.profile.vue @@ -13,7 +13,7 @@ <span v-if="user.isMuted">%fa:eye% %i18n:@unmute%</span> <span v-if="!user.isMuted">%fa:eye-slash% %i18n:@mute%</span> </button> - <button class="mute ui" @click="list">%fa:list% リストに追加</button> + <button class="mute ui" @click="list">%fa:list% %i18n:@push-to-a-list%</button> </div> </div> </template> @@ -76,7 +76,7 @@ export default Vue.extend({ }); (this as any).apis.dialog({ title: 'Done!', - text: `${this.user.name}を${list.title}に追加しました。` + text: '%i18n:@list-pushed%'.replace('{user}', this.user.name).replace('{list}', list.title) }); }); } diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue index 1a83f8134..300fd68f0 100644 --- a/src/client/app/desktop/views/pages/user/user.vue +++ b/src/client/app/desktop/views/pages/user/user.vue @@ -68,7 +68,6 @@ export default Vue.extend({ this.user = user; this.fetching = false; Progress.done(); - document.title = getUserName(this.user) + ' | %i18n:common.name%'; }); }, diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue index 9543a55b9..585a23de2 100644 --- a/src/client/app/desktop/views/pages/welcome.vue +++ b/src/client/app/desktop/views/pages/welcome.vue @@ -8,7 +8,7 @@ <div class="body" :style="{ backgroundImage: `url('${ welcomeBgUrl }')` }"> <div class="container"> <div class="info"> - <span>%i18n:common.name% <b>{{ host }}</b></span> + <span><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> @@ -16,9 +16,9 @@ </div> <main> <div class="about"> - <h1 v-if="name">{{ name }}</h1> - <h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="%i18n:common.name%"></h1> - <p class="powerd-by" v-if="name">%i18n:@powered-by-misskey%</p> + <h1 v-if="name != 'Misskey'">{{ name }}</h1> + <h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"></h1> + <p class="powerd-by" v-if="name != 'Misskey'" v-html="'%i18n:@powered-by-misskey%'"></p> <p class="desc" v-html="description || '%i18n:common.about%'"></p> <a ref="signup" @click="signup">📦 %i18n:@signup%</a> </div> @@ -32,7 +32,7 @@ <mk-nav class="nav"/> </div> <mk-forkit class="forkit"/> - <img src="assets/title.dark.svg" alt="%i18n:common.name%"> + <img src="assets/title.dark.svg" :alt="name"> </div> <div class="tl"> <mk-welcome-timeline :max="20"/> diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue index 7421a8110..8ff0bb5d0 100644 --- a/src/client/app/desktop/views/widgets/polls.vue +++ b/src/client/app/desktop/views/widgets/polls.vue @@ -6,8 +6,8 @@ <div class="mkw-polls--body" :data-darkmode="$store.state.device.darkmode"> <div class="poll" v-if="!fetching && poll != null"> - <p v-if="poll.text"><router-link to="poll | notePage">{{ poll.text }}</router-link></p> - <p v-if="!poll.text"><router-link to="poll | notePage">%fa:link%</router-link></p> + <p v-if="poll.text"><router-link :to="poll | notePage">{{ poll.text }}</router-link></p> + <p v-if="!poll.text"><router-link :to="poll | notePage">%fa:link%</router-link></p> <mk-poll :note="poll"/> </div> <p class="empty" v-if="!fetching && poll == null">%i18n:@nothing%</p> diff --git a/src/client/app/desktop/views/widgets/post-form.vue b/src/client/app/desktop/views/widgets/post-form.vue index 618d19efc..19a2790d9 100644 --- a/src/client/app/desktop/views/widgets/post-form.vue +++ b/src/client/app/desktop/views/widgets/post-form.vue @@ -55,7 +55,7 @@ export default define({ }).then(data => { this.clear(); }).catch(err => { - alert('失敗した'); + alert('Something happened'); }).then(() => { this.posting = false; }); diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue index 9702aaa90..a22607b61 100644 --- a/src/client/app/desktop/views/widgets/profile.vue +++ b/src/client/app/desktop/views/widgets/profile.vue @@ -6,10 +6,11 @@ <div class="banner" :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''" title="%i18n:@update-banner%" - @click="os.apis.updateBanner" + @click="() => os.apis.updateBanner()" ></div> <mk-avatar class="avatar" :user="$store.state.i" - @click="os.apis.updateAvatar" + :disable-link="true" + @click="() => os.apis.updateAvatar()" title="%i18n:@update-avatar%" /> <router-link class="name" :to="$store.state.i | userPage">{{ $store.state.i | userName }}</router-link> diff --git a/src/client/app/dev/views/app.vue b/src/client/app/dev/views/app.vue index a35b032b7..54d99ec82 100644 --- a/src/client/app/dev/views/app.vue +++ b/src/client/app/dev/views/app.vue @@ -1,6 +1,6 @@ <template> <mk-ui> - <p v-if="fetching">読み込み中</p> + <p v-if="fetching">%i18n:common.loading%</p> <b-card v-if="!fetching" :header="app.name"> <b-form-group label="App Secret"> <b-input :value="app.secret" readonly/> diff --git a/src/client/app/dev/views/index.vue b/src/client/app/dev/views/index.vue index 3f572b390..bb137481f 100644 --- a/src/client/app/dev/views/index.vue +++ b/src/client/app/dev/views/index.vue @@ -1,6 +1,6 @@ <template> <mk-ui> - <b-button to="/apps" variant="primary">アプリの管理</b-button> + <b-button to="/apps" variant="primary">%i18n:@manage-apps%</b-button> </mk-ui> </template> diff --git a/src/client/app/dev/views/ui.vue b/src/client/app/dev/views/ui.vue index 0a1cdf829..f1e001909 100644 --- a/src/client/app/dev/views/ui.vue +++ b/src/client/app/dev/views/ui.vue @@ -1,7 +1,7 @@ <template> <div> <b-navbar toggleable="md" type="dark" variant="info"> - <b-navbar-brand>%i18n:common.name% Developers</b-navbar-brand> + <b-navbar-brand>Developers</b-navbar-brand> <b-navbar-nav> <b-nav-item to="/">Home</b-nav-item> <b-nav-item to="/apps">Apps</b-nav-item> diff --git a/src/client/app/init.ts b/src/client/app/init.ts index 043f26d0b..18f510ea2 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -55,7 +55,7 @@ Vue.mixin({ console.info(`Misskey v${version} (${codename})`); console.info( - '%cここにコードを入力したり張り付けたりしないでください。アカウントが不正利用される可能性があります。', + '%c%i18n:common.do-not-copy-paste%', 'color: red; background: yellow; font-size: 16px; font-weight: bold;'); // BootTimer解除 diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue index cba8ef180..aed372d9a 100644 --- a/src/client/app/mobile/views/components/notes.vue +++ b/src/client/app/mobile/views/components/notes.vue @@ -38,6 +38,7 @@ <script lang="ts"> import Vue from 'vue'; import getNoteSummary from '../../../../../misc/get-note-summary'; +import * as config from '../../../config'; const displayLimit = 30; @@ -66,7 +67,7 @@ export default Vue.extend({ const date = new Date(note.createdAt).getDate(); const month = new Date(note.createdAt).getMonth() + 1; note._date = date; - note._datetext = `${month}月 ${date}日`; + note._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString()); return note; }); } @@ -183,7 +184,7 @@ export default Vue.extend({ clearNotification() { this.unreadCount = 0; - document.title = '%i18n:common.name%'; + document.title = config.name; }, onVisibilitychange() { diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue index fc220c252..9f20c3fb2 100644 --- a/src/client/app/mobile/views/components/notifications.vue +++ b/src/client/app/mobile/views/components/notifications.vue @@ -42,7 +42,7 @@ export default Vue.extend({ const date = new Date(notification.createdAt).getDate(); const month = new Date(notification.createdAt).getMonth() + 1; notification._date = date; - notification._datetext = `${month}月 ${date}日`; + notification._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString()); return notification; }); } diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue index 794ec9a30..b87c6f1eb 100644 --- a/src/client/app/mobile/views/components/ui.header.vue +++ b/src/client/app/mobile/views/components/ui.header.vue @@ -3,12 +3,12 @@ <mk-special-message/> <div class="main" ref="main"> <div class="backdrop"></div> - <p ref="welcomeback" v-if="$store.getters.isSignedIn">%i18n:@welcome-back%<b>{{ $store.state.i | userName }}</b>さん</p> + <p ref="welcomeback" v-if="$store.getters.isSignedIn">%i18n:@welcome-back%<b>{{ $store.state.i | userName }}</b>%i18n:@adjective%</p> <div class="content" ref="mainContainer"> <button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button> <template v-if="hasUnreadNotification || hasUnreadMessagingMessage || hasGameInvitation">%fa:circle%</template> <h1> - <slot>%i18n:common.name%</slot> + <slot>config.name</slot> </h1> <slot name="func"></slot> </div> @@ -20,11 +20,13 @@ <script lang="ts"> import Vue from 'vue'; import * as anime from 'animejs'; +import * as config from '../../../config'; export default Vue.extend({ props: ['func'], data() { return { + config, hasGameInvitation: false, connection: null, connectionId: null diff --git a/src/client/app/mobile/views/pages/drive.vue b/src/client/app/mobile/views/pages/drive.vue index 9c635be05..72427a478 100644 --- a/src/client/app/mobile/views/pages/drive.vue +++ b/src/client/app/mobile/views/pages/drive.vue @@ -25,6 +25,7 @@ <script lang="ts"> import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; +import * as config from '../../../config'; export default Vue.extend({ data() { @@ -43,7 +44,7 @@ export default Vue.extend({ window.addEventListener('popstate', this.onPopState); }, mounted() { - document.title = '%i18n:common.name% Drive'; + document.title = `${config.name} Drive`; document.documentElement.style.background = '#fff'; }, beforeDestroy() { @@ -63,7 +64,7 @@ export default Vue.extend({ (this.$refs as any).browser.openContextMenu(); }, onMoveRoot(silent) { - const title = '%i18n:common.name% Drive'; + const title = `${config.name} Drive`; if (!silent) { // Rewrite URL @@ -76,7 +77,7 @@ export default Vue.extend({ this.folder = null; }, onOpenFolder(folder, silent) { - const title = folder.name + ' | %i18n:common.name% Drive'; + const title = `${folder.name} | ${config.name} Drive`; if (!silent) { // Rewrite URL @@ -89,7 +90,7 @@ export default Vue.extend({ this.folder = folder; }, onOpenFile(file, silent) { - const title = file.name + ' | %i18n:common.name% Drive'; + const title = `${file.name} | ${config.name} Drive`; if (!silent) { // Rewrite URL diff --git a/src/client/app/mobile/views/pages/favorites.vue b/src/client/app/mobile/views/pages/favorites.vue index 88a84bd8a..491890bb0 100644 --- a/src/client/app/mobile/views/pages/favorites.vue +++ b/src/client/app/mobile/views/pages/favorites.vue @@ -14,6 +14,7 @@ <script lang="ts"> import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; +import * as config from '../../../config'; export default Vue.extend({ data() { @@ -28,7 +29,7 @@ export default Vue.extend({ this.fetch(); }, mounted() { - document.title = '%i18n:common.name% | %i18n:@notifications%'; + document.title = `${config.name} | %i18n:@notifications%`; }, methods: { fetch() { diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue index 4956eb1b9..5bba534ba 100644 --- a/src/client/app/mobile/views/pages/followers.vue +++ b/src/client/app/mobile/views/pages/followers.vue @@ -21,6 +21,7 @@ import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; import parseAcct from '../../../../../misc/acct/parse'; import getUserName from '../../../../../misc/get-user-name'; +import * as config from '../../../config'; export default Vue.extend({ data() { @@ -49,7 +50,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; - document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | %i18n:common.name%'; + document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | ' + config.name; }); }, onLoaded() { diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue index fa6807a24..cdc009b76 100644 --- a/src/client/app/mobile/views/pages/following.vue +++ b/src/client/app/mobile/views/pages/following.vue @@ -20,6 +20,7 @@ import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; import parseAcct from '../../../../../misc/acct/parse'; +import * as config from '../../../config'; export default Vue.extend({ data() { @@ -48,7 +49,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; - document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | %i18n:common.name%'; + document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | ' + config.name; }); }, onLoaded() { diff --git a/src/client/app/mobile/views/pages/games/reversi.vue b/src/client/app/mobile/views/pages/games/reversi.vue index 7118644ef..448c9b8d7 100644 --- a/src/client/app/mobile/views/pages/games/reversi.vue +++ b/src/client/app/mobile/views/pages/games/reversi.vue @@ -1,21 +1,27 @@ <template> <mk-ui> <span slot="header">%fa:gamepad%%i18n:@reversi%</span> - <mk-reversi :game-id="$route.params.game" @nav="nav"/> + <mk-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/> </mk-ui> </template> <script lang="ts"> import Vue from 'vue'; +import * as config from '../../../../config'; export default Vue.extend({ mounted() { - document.title = '%i18n:common.name% %i18n:@reversi%'; + document.title = `${config.name} %i18n:@reversi%`; document.documentElement.style.background = '#fff'; }, methods: { - nav(game) { - history.pushState(null, null, '/reversi/' + game.id); + nav(game, actualNav) { + if (actualNav) { + this.$router.push('/reversi/' + game.id); + } else { + // TODO: https://github.com/vuejs/vue-router/issues/703 + this.$router.push('/reversi/' + game.id); + } } } }); diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index 7b14c7ee9..c1ed97ac1 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -49,6 +49,7 @@ import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; import XTl from './home.timeline.vue'; +import * as config from '../../../config'; export default Vue.extend({ components: { @@ -96,7 +97,7 @@ export default Vue.extend({ }, mounted() { - document.title = '%i18n:common.name%'; + document.title = config.name; Progress.start(); diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue index 24ffc658a..e2016fc82 100644 --- a/src/client/app/mobile/views/pages/messaging-room.vue +++ b/src/client/app/mobile/views/pages/messaging-room.vue @@ -11,6 +11,7 @@ <script lang="ts"> import Vue from 'vue'; import parseAcct from '../../../../../misc/acct/parse'; +import * as config from '../../../config'; export default Vue.extend({ data() { @@ -47,7 +48,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; - document.title = `%i18n:@messaging%: ${Vue.filter('userName')(this.user)} | %i18n:common.name%`; + document.title = `%i18n:@messaging%: ${Vue.filter('userName')(this.user)} | ${config.name}`; }); } } diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue index b5a4f405f..9f2beb860 100644 --- a/src/client/app/mobile/views/pages/messaging.vue +++ b/src/client/app/mobile/views/pages/messaging.vue @@ -8,10 +8,11 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../misc/acct/render'; +import * as config from '../../../config'; export default Vue.extend({ mounted() { - document.title = '%i18n:common.name% %i18n:@messaging%'; + document.title = `${config.name} %i18n:@messaging%`; }, methods: { navigate(user) { diff --git a/src/client/app/mobile/views/pages/note.vue b/src/client/app/mobile/views/pages/note.vue index 64d46f051..8b1095c50 100644 --- a/src/client/app/mobile/views/pages/note.vue +++ b/src/client/app/mobile/views/pages/note.vue @@ -16,6 +16,7 @@ <script lang="ts"> import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; +import * as config from '../../../config'; export default Vue.extend({ data() { @@ -31,7 +32,7 @@ export default Vue.extend({ this.fetch(); }, mounted() { - document.title = '%i18n:common.name%'; + document.title = config.name; }, methods: { fetch() { diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue index 368872161..4d3c8ee53 100644 --- a/src/client/app/mobile/views/pages/notifications.vue +++ b/src/client/app/mobile/views/pages/notifications.vue @@ -15,7 +15,7 @@ import Progress from '../../../common/scripts/loading'; export default Vue.extend({ mounted() { - document.title = '%i18n:common.name% | %i18n:@notifications%'; + document.title = '%i18n:@notifications%'; Progress.start(); }, diff --git a/src/client/app/mobile/views/pages/received-follow-requests.vue b/src/client/app/mobile/views/pages/received-follow-requests.vue index fff2fdea5..77938c3d6 100644 --- a/src/client/app/mobile/views/pages/received-follow-requests.vue +++ b/src/client/app/mobile/views/pages/received-follow-requests.vue @@ -25,7 +25,7 @@ export default Vue.extend({ }; }, mounted() { - document.title = '%i18n:common.name% | %i18n:@title%'; + document.title = '%i18n:@title%'; Progress.start(); diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue index 7801068c1..0b37a3c7b 100644 --- a/src/client/app/mobile/views/pages/search.vue +++ b/src/client/app/mobile/views/pages/search.vue @@ -3,7 +3,7 @@ <span slot="header">%fa:search% {{ q }}</span> <main> - <p v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p> + <p :class="$style.empty" v-if="!fetching && empty">%fa:search% {{ '%i18n:not-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:not-found%'.split('{}')[1] }}</p> <mk-notes ref="timeline" :more="existMore ? more : null"/> </main> </mk-ui> @@ -12,6 +12,7 @@ <script lang="ts"> import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; +import * as config from '../../../config'; const limit = 20; @@ -34,7 +35,7 @@ export default Vue.extend({ } }, mounted() { - document.title = `%i18n:@search%: ${this.q} | %i18n:common.name%`; + document.title = `%i18n:@search%: ${this.q} | ${config.name}`; this.fetch(); }, diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index 73bff55e4..f74b734b6 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -14,6 +14,7 @@ <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> + <ui-switch v-model="$store.state.settings.disableAnimatedMfm" @change="onChangeDisableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch> <div> <div>%i18n:@timeline%</div> @@ -142,7 +143,7 @@ export default Vue.extend({ }, mounted() { - document.title = '%i18n:common.name% | %i18n:@settings%'; + document.title = '%i18n:@settings%'; }, methods: { @@ -192,6 +193,13 @@ export default Vue.extend({ }); }, + onChangeDisableAnimatedMfm(v) { + this.$store.dispatch('settings/set', { + key: 'disableAnimatedMfm', + value: v + }); + }, + onChangeShowReplyTarget(v) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', diff --git a/src/client/app/mobile/views/pages/share.vue b/src/client/app/mobile/views/pages/share.vue index 3e33e3273..dcb55e670 100644 --- a/src/client/app/mobile/views/pages/share.vue +++ b/src/client/app/mobile/views/pages/share.vue @@ -1,21 +1,23 @@ <template> <div class="azibmfpleajagva420swmu4c3r7ni7iw"> - <h1>{{'%i18n:@share-with%'.split("{}")[0] + '%i18n:common.name%' + '%i18n:@share-with%'.split("{}")[1]}}</h1> + <h1>{{ '%i18n:@share-with%'.replace('{}', name) }}</h1> <div> <mk-signin v-if="!$store.getters.isSignedIn"/> <mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/> <p v-if="posted" class="posted">%fa:check%</p> </div> - <ui-button class="close" v-if="posted" @click="close">閉じる</ui-button> + <ui-button class="close" v-if="posted" @click="close">%i18n:common.close%</ui-button> </div> </template> <script lang="ts"> import Vue from 'vue'; +import * as config from '../../../config'; export default Vue.extend({ data() { return { + name: config.name, posted: false, text: new URLSearchParams(location.search).get('text') }; diff --git a/src/client/app/mobile/views/pages/user-lists.vue b/src/client/app/mobile/views/pages/user-lists.vue index 1cce3e9bd..abd04c149 100644 --- a/src/client/app/mobile/views/pages/user-lists.vue +++ b/src/client/app/mobile/views/pages/user-lists.vue @@ -23,7 +23,7 @@ export default Vue.extend({ }; }, mounted() { - document.title = '%i18n:common.name% | %i18n:@title%'; + document.title = '%i18n:@title%'; Progress.start(); diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index d01634571..11ca1caeb 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -67,6 +67,7 @@ import * as age from 's-age'; import parseAcct from '../../../../../misc/acct/parse'; import Progress from '../../../common/scripts/loading'; import XHome from './user/home.vue'; +import * as config from '../../../config'; export default Vue.extend({ components: { @@ -106,7 +107,7 @@ export default Vue.extend({ this.fetching = false; Progress.done(); - document.title = Vue.filter('userName')(this.user) + ' | %i18n:common.name%'; + document.title = Vue.filter('userName')(this.user) + ' | ' + config.name; }); } } diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue index acc8e2c49..f8a7ff1c8 100644 --- a/src/client/app/mobile/views/pages/welcome.vue +++ b/src/client/app/mobile/views/pages/welcome.vue @@ -1,10 +1,10 @@ <template> <div class="welcome"> <div> - <img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="%i18n:common.name%"> + <img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"> <p class="host">{{ host }}</p> <div class="about"> - <h2>{{ name || 'unidentified' }}</h2> + <h2>{{ name }}</h2> <p v-html="description || '%i18n:common.about%'"></p> <router-link class="signup" to="/signup">%i18n:@signup%</router-link> </div> diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue index 4400132bf..b90d710c6 100644 --- a/src/client/app/mobile/views/pages/widgets.vue +++ b/src/client/app/mobile/views/pages/widgets.vue @@ -24,8 +24,8 @@ <option value="nav">%i18n:common.widgets.nav%</option> <option value="tips">%i18n:common.widgets.tips%</option> </select> - <button @click="addWidget">追加</button> - <p><a @click="hint">カスタマイズのヒント</a></p> + <button @click="addWidget">%i18n:add-widget%</button> + <p><a @click="hint">%i18n:customization-tips%</a></p> </header> <x-draggable :list="widgets" @@ -53,6 +53,7 @@ import Vue from 'vue'; import * as XDraggable from 'vuedraggable'; import * as uuid from 'uuid'; +import * as config from '../../../config'; export default Vue.extend({ components: { @@ -102,7 +103,7 @@ export default Vue.extend({ }, mounted() { - document.title = '%i18n:common.name%'; + document.title = config.name; }, methods: { diff --git a/src/config/load.ts b/src/config/load.ts index 44a24c96a..1c59f82b3 100644 --- a/src/config/load.ts +++ b/src/config/load.ts @@ -47,6 +47,8 @@ export default function load() { if (config.localDriveCapacityMb == null) config.localDriveCapacityMb = 256; if (config.remoteDriveCapacityMb == null) config.remoteDriveCapacityMb = 8; + if (config.name == null) config.name = 'Misskey'; + return Object.assign(config, mixin); } diff --git a/src/docs/api/entities/drive-folder.yaml b/src/docs/api/entities/drive-folder.yaml new file mode 100644 index 000000000..0fb8308dd --- /dev/null +++ b/src/docs/api/entities/drive-folder.yaml @@ -0,0 +1,41 @@ +name: "DriveFolder" + +desc: + ja: "ドライブのフォルダを表します。" + en: "A folder of Drive." + +props: + id: + type: "id" + optional: false + desc: + ja: "フォルダID" + en: "The ID of this folder" + + createdAt: + type: "date" + optional: false + desc: + ja: "作成日時" + en: "The created date of this folder" + + userId: + type: "id(User)" + optional: false + desc: + ja: "所有者ID" + en: "The ID of the owner of this folder" + + parentId: + type: "entity(DriveFolder)" + optional: false + desc: + ja: "親フォルダのID (ルートなら null)" + en: "The ID of parent folder" + + name: + type: "string" + optional: false + desc: + ja: "フォルダ名" + en: "The name of this folder" diff --git a/src/docs/reversi-bot.ja.md b/src/docs/reversi-bot.ja.md index 6fe62003d..98b543ca6 100644 --- a/src/docs/reversi-bot.ja.md +++ b/src/docs/reversi-bot.ja.md @@ -1,15 +1,15 @@ # MisskeyリバーシBotの開発 Misskeyのリバーシ機能に対応したBotの開発方法をここに記します。 -1. `reversi`ストリームに以下のパラメータを付けて接続する: +1. `games/reversi`ストリームに以下のパラメータを付けて接続する: * `i`: botアカウントのAPIキー 2. 対局への招待が来たら、ストリームから`invited`イベントが流れてくる * イベントの中身に、`parent`という名前で対局へ誘ってきたユーザーの情報が含まれている -3. `reversi/match`へ、`user_id`として`parent`の`id`が含まれたリクエストを送信する +3. `games/reversi/match`へ、`user_id`として`parent`の`id`が含まれたリクエストを送信する -4. 上手くいくとゲーム情報が返ってくるので、`reversi-game`ストリームへ、以下のパラメータを付けて接続する: +4. 上手くいくとゲーム情報が返ってくるので、`games/reversi-game`ストリームへ、以下のパラメータを付けて接続する: * `i`: botアカウントのAPIキー * `game`: `game`の`id` @@ -96,8 +96,8 @@ y = Math.floor(pos / mapWidth) フォームコントロールは、次のようなオブジェクトです: ```javascript { - id: 'button1', - type: 'button', + id: 'switch1', + type: 'switch', label: 'Enable hoge', value: false } @@ -110,21 +110,21 @@ y = Math.floor(pos / mapWidth) ### フォームの操作を受け取る ユーザーがフォームを操作すると、ストリームから`update-form`イベントが流れてきます。 イベントの中身には、コントロールのIDと、ユーザーが設定した値が含まれています。 -例えば、上で示したボタンをユーザーがオンにしたとすると、次のイベントが流れてきます: +例えば、上で示したスイッチをユーザーがオンにしたとすると、次のイベントが流れてきます: ```javascript { - id: 'button1', + id: 'switch1', value: true } ``` ### フォームコントロールの種類 -#### ボタン -type: `button` -ボタンを表示します。何かの機能をオン/オフさせたい場合に有用です。 +#### スイッチ +type: `switch` +スイッチを表示します。何かの機能をオン/オフさせたい場合に有用です。 ##### プロパティ -`desc` ... ボタンの詳細な説明。 +`desc` ... スイッチの詳細な説明。 #### ラジオボタン type: `radio` @@ -145,6 +145,15 @@ items: [{ }] ``` +#### スライダー +type: `slider` +スライダーを表示します。 + +##### プロパティ +`min` ... スライダーの下限。 +`max` ... スライダーの上限。 +`step` ... 入力欄で刻むステップ値。 + #### テキストボックス type: `textbox` テキストボックスを表示します。ユーザーになにか入力させる一般的な用途に利用できます。 @@ -163,3 +172,6 @@ type: `textbox` } ``` メッセージの種類: `success`, `info`, `warning`, `error`。 + +## 投了する +投了をするには、<a href="./api/endpoints/games/reversi/games/surrender">このエンドポイント</a>にリクエストします。 diff --git a/src/games/reversi/package.json b/src/games/reversi/package.json new file mode 100644 index 000000000..5e7fdcb58 --- /dev/null +++ b/src/games/reversi/package.json @@ -0,0 +1,18 @@ +{ + "name": "misskey-reversi", + "version": "0.0.5", + "description": "Misskey reversi engine", + "keywords": [ + "misskey" + ], + "author": "syuilo <i@syuilo.com>", + "license": "MIT", + "repository": "https://github.com/syuilo/misskey.git", + "bugs": "https://github.com/syuilo/misskey/issues", + "main": "./built/core.js", + "types": "./built/core.d.ts", + "scripts": { + "build": "tsc" + }, + "dependencies": {} +} diff --git a/src/games/reversi/tsconfig.json b/src/games/reversi/tsconfig.json new file mode 100644 index 000000000..851fb6b7e --- /dev/null +++ b/src/games/reversi/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "noEmitOnError": false, + "noImplicitAny": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "experimentalDecorators": true, + "declaration": true, + "sourceMap": false, + "target": "es2017", + "module": "commonjs", + "removeComments": false, + "noLib": false, + "outDir": "./built", + "rootDir": "./" + }, + "compileOnSave": false, + "include": [ + "./core.ts" + ] +} diff --git a/src/index.ts b/src/index.ts index 18eff8176..0dda8b05b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,8 @@ Error.stackTraceLimit = Infinity; +require('events').EventEmitter.defaultMaxListeners = 128; + import * as os from 'os'; import * as cluster from 'cluster'; import * as debug from 'debug'; diff --git a/src/mfm/html.ts b/src/mfm/html.ts index 71b473947..c11bd55cf 100644 --- a/src/mfm/html.ts +++ b/src/mfm/html.ts @@ -1,5 +1,6 @@ const { lib: emojilib } = require('emojilib'); -import { JSDOM } from 'jsdom'; +const jsdom = require('jsdom'); +const { JSDOM } = jsdom; import config from '../config'; import { INote } from '../models/note'; import { TextElement } from './parse'; @@ -11,6 +12,18 @@ const handlers: { [key: string]: (window: any, token: any, mentionedRemoteUsers: document.body.appendChild(b); }, + big({ document }, { big }) { + const b = document.createElement('strong'); + b.textContent = big; + document.body.appendChild(b); + }, + + motion({ document }, { big }) { + const b = document.createElement('strong'); + b.textContent = big; + document.body.appendChild(b); + }, + code({ document }, { code }) { const pre = document.createElement('pre'); const inner = document.createElement('code'); diff --git a/src/mfm/parse/elements/big.ts b/src/mfm/parse/elements/big.ts new file mode 100644 index 000000000..8e39c75a5 --- /dev/null +++ b/src/mfm/parse/elements/big.ts @@ -0,0 +1,20 @@ +/** + * Big + */ + +export type TextElementBig = { + type: 'big' + content: string + big: string +}; + +export default function(text: string) { + const match = text.match(/^\*\*\*(.+?)\*\*\*/); + if (!match) return null; + const big = match[0]; + return { + type: 'big', + content: big, + big: match[1] + } as TextElementBig; +} diff --git a/src/mfm/parse/elements/motion.ts b/src/mfm/parse/elements/motion.ts new file mode 100644 index 000000000..9e7370e07 --- /dev/null +++ b/src/mfm/parse/elements/motion.ts @@ -0,0 +1,20 @@ +/** + * Motion + */ + +export type TextElementMotion = { + type: 'motion' + content: string + motion: string +}; + +export default function(text: string) { + const match = text.match(/^\(\(\((.+?)\)\)\)/) || text.match(/^<motion>(.+?)<\/motion>/); + if (!match) return null; + const motion = match[0]; + return { + type: 'motion', + content: motion, + motion: match[1] + } as TextElementMotion; +} diff --git a/src/mfm/parse/index.ts b/src/mfm/parse/index.ts index 8d71409e5..99c00ae64 100644 --- a/src/mfm/parse/index.ts +++ b/src/mfm/parse/index.ts @@ -3,6 +3,7 @@ */ import { TextElementBold } from './elements/bold'; +import { TextElementBig } from './elements/big'; import { TextElementCode } from './elements/code'; import { TextElementEmoji } from './elements/emoji'; import { TextElementHashtag } from './elements/hashtag'; @@ -13,8 +14,10 @@ import { TextElementQuote } from './elements/quote'; import { TextElementSearch } from './elements/search'; import { TextElementTitle } from './elements/title'; import { TextElementUrl } from './elements/url'; +import { TextElementMotion } from './elements/motion'; const elements = [ + require('./elements/big'), require('./elements/bold'), require('./elements/title'), require('./elements/url'), @@ -25,11 +28,13 @@ const elements = [ require('./elements/inline-code'), require('./elements/quote'), require('./elements/emoji'), - require('./elements/search') + require('./elements/search'), + require('./elements/motion') ].map(element => element.default as TextElementProcessor); export type TextElement = { type: 'text', content: string } | TextElementBold + | TextElementBig | TextElementCode | TextElementEmoji | TextElementHashtag @@ -39,7 +44,8 @@ export type TextElement = { type: 'text', content: string } | TextElementQuote | TextElementSearch | TextElementTitle - | TextElementUrl; + | TextElementUrl + | TextElementMotion; export type TextElementProcessor = (text: string, i: number) => TextElement | TextElement[]; export default (source: string): TextElement[] => { diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts index 33c045190..ad5496d7c 100644 --- a/src/models/drive-file.ts +++ b/src/models/drive-file.ts @@ -10,7 +10,7 @@ import DriveFileThumbnail, { deleteDriveFileThumbnail } from './drive-file-thumb const DriveFile = monkDb.get<IDriveFile>('driveFiles.files'); DriveFile.createIndex('md5'); -DriveFile.createIndex('metadata.uri', { sparse: true, unique: true }); +DriveFile.createIndex(['metadata.uri', 'metadata.userId'], { sparse: true, unique: true }); export default DriveFile; export const DriveFileChunk = monkDb.get('driveFiles.chunks'); diff --git a/src/models/games/reversi/game.ts b/src/models/games/reversi/game.ts index 8255db058..6a6c6463d 100644 --- a/src/models/games/reversi/game.ts +++ b/src/models/games/reversi/game.ts @@ -25,6 +25,7 @@ export interface IReversiGame { isStarted: boolean; isEnded: boolean; winnerId: mongo.ObjectID; + surrendered: mongo.ObjectID; logs: Array<{ at: Date; color: boolean; diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts index e06866da4..e14a16210 100644 --- a/src/queue/processors/http/deliver.ts +++ b/src/queue/processors/http/deliver.ts @@ -7,6 +7,11 @@ export default async (job: bq.Job, done: any): Promise<void> => { await request(job.data.user, job.data.to, job.data.content); done(); } catch (res) { + if (res == null || !res.hasOwnProperty('statusCode')) { + console.warn(`deliver failed (unknown): ${res}`); + return done(); + } + if (res.statusCode == null) return done(); if (res.statusCode >= 400 && res.statusCode < 500) { // HTTPステータスコード4xxはクライアントエラーであり、それはつまり diff --git a/src/server/api/endpoints/games/reversi/games/surrender.ts b/src/server/api/endpoints/games/reversi/games/surrender.ts new file mode 100644 index 000000000..49821650e --- /dev/null +++ b/src/server/api/endpoints/games/reversi/games/surrender.ts @@ -0,0 +1,59 @@ +import $ from 'cafy'; import ID from '../../../../../../misc/cafy-id'; +import ReversiGame, { pack } from '../../../../../../models/games/reversi/game'; +import { ILocalUser } from '../../../../../../models/user'; +import getParams from '../../../../get-params'; +import { publishReversiGameStream } from '../../../../../../stream'; + +export const meta = { + desc: { + ja: '指定したリバーシの対局で投了します。' + }, + + requireCredential: true, + + params: { + gameId: $.type(ID).note({ + desc: { + ja: '投了したい対局' + } + }) + } +}; + +export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); + + const game = await ReversiGame.findOne({ _id: ps.gameId }); + + if (game == null) { + return rej('game not found'); + } + + if (game.isEnded) { + return rej('this game is already ended'); + } + + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) { + return rej('access denied'); + } + + const winnerId = game.user1Id.equals(user._id) ? game.user2Id : game.user1Id; + + await ReversiGame.update({ + _id: game._id + }, { + $set: { + surrendered: user._id, + isEnded: true, + winnerId: winnerId + } + }); + + publishReversiGameStream(game._id, 'ended', { + winnerId: winnerId, + game: await pack(game._id, user) + }); + + res(); +}); diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts index d443d35b4..eda3f9572 100644 --- a/src/server/api/endpoints/users/search.ts +++ b/src/server/api/endpoints/users/search.ts @@ -1,33 +1,156 @@ import $ from 'cafy'; -import User, { pack, ILocalUser } from '../../../../models/user'; const escapeRegexp = require('escape-regexp'); +import User, { pack, ILocalUser, validateUsername, IUser } from '../../../../models/user'; +import getParams from '../../get-params'; + +export const meta = { + desc: { + ja: 'ユーザーを検索します。' + }, + + requireCredential: false, + + params: { + query: $.str.note({ + desc: { + ja: 'クエリ' + } + }), + + offset: $.num.optional.min(0).note({ + default: 0, + desc: { + ja: 'オフセット' + } + }), + + limit: $.num.optional.range(1, 100).note({ + default: 10, + desc: { + ja: '取得する数' + } + }), + + localOnly: $.bool.optional.note({ + default: false, + desc: { + ja: 'ローカルユーザーのみ検索対象にするか否か' + } + }), + }, +}; /** * Search a user */ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { - // Get 'query' parameter - const [query, queryError] = $.str.pipe(x => x != '').get(params.query); - if (queryError) return rej('invalid query param'); + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); - // Get 'max' parameter - const [max = 10, maxErr] = $.num.optional.range(1, 30).get(params.max); - if (maxErr) return rej('invalid max param'); + const isUsername = validateUsername(ps.query.replace('@', '')); - const escapedQuery = escapeRegexp(query); + let users: IUser[] = []; - // Search users - const users = await User - .find({ - host: null, - $or: [{ - usernameLower: new RegExp(escapedQuery.replace('@', '').toLowerCase()) + if (isUsername) { + users = await User + .find({ + host: null, + usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())) }, { - name: new RegExp(escapedQuery) - }] - }, { - limit: max - }); + limit: ps.limit, + skip: ps.offset + }); + + if (users.length < ps.limit && !ps.localOnly) { + const otherUsers = await User + .find({ + host: { $ne: null }, + usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())) + }, { + limit: ps.limit - users.length + }); + + users = users.concat(otherUsers); + } + + if (users.length < ps.limit) { + const otherUsers = await User + .find({ + _id: { $nin: users.map(u => u._id) }, + host: null, + usernameLower: new RegExp(escapeRegexp(ps.query.replace('@', '').toLowerCase())) + }, { + limit: ps.limit - users.length + }); + + users = users.concat(otherUsers); + } + + if (users.length < ps.limit && !ps.localOnly) { + const otherUsers = await User + .find({ + _id: { $nin: users.map(u => u._id) }, + host: { $ne: null }, + usernameLower: new RegExp(escapeRegexp(ps.query.replace('@', '').toLowerCase())) + }, { + limit: ps.limit - users.length + }); + + users = users.concat(otherUsers); + } + } + + if (users.length < ps.limit) { + const otherUsers = await User + .find({ + _id: { $nin: users.map(u => u._id) }, + host: null, + name: new RegExp('^' + escapeRegexp(ps.query.toLowerCase())) + }, { + limit: ps.limit - users.length + }); + + users = users.concat(otherUsers); + } + + if (users.length < ps.limit && !ps.localOnly) { + const otherUsers = await User + .find({ + _id: { $nin: users.map(u => u._id) }, + host: { $ne: null }, + name: new RegExp('^' + escapeRegexp(ps.query.toLowerCase())) + }, { + limit: ps.limit - users.length + }); + + users = users.concat(otherUsers); + } + + if (users.length < ps.limit) { + const otherUsers = await User + .find({ + _id: { $nin: users.map(u => u._id) }, + host: null, + name: new RegExp(escapeRegexp(ps.query.toLowerCase())) + }, { + limit: ps.limit - users.length + }); + + users = users.concat(otherUsers); + } + + if (users.length < ps.limit && !ps.localOnly) { + const otherUsers = await User + .find({ + _id: { $nin: users.map(u => u._id) }, + host: { $ne: null }, + name: new RegExp(escapeRegexp(ps.query.toLowerCase())) + }, { + limit: ps.limit - users.length + }); + + users = users.concat(otherUsers); + } // Serialize res(await Promise.all(users.map(user => pack(user, me, { detail: true })))); diff --git a/src/server/api/endpoints/users/search_by_username.ts b/src/server/api/endpoints/users/search_by_username.ts deleted file mode 100644 index bfab37838..000000000 --- a/src/server/api/endpoints/users/search_by_username.ts +++ /dev/null @@ -1,70 +0,0 @@ -import $ from 'cafy'; -import User, { pack, ILocalUser } from '../../../../models/user'; -const escapeRegexp = require('escape-regexp'); - -/** - * Search a user by username - */ -export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { - // Get 'query' parameter - const [query, queryError] = $.str.get(params.query); - if (queryError) return rej('invalid query param'); - - // Get 'offset' parameter - const [offset = 0, offsetErr] = $.num.optional.min(0).get(params.offset); - if (offsetErr) return rej('invalid offset param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit); - if (limitErr) return rej('invalid limit param'); - - let users = await User - .find({ - host: null, - usernameLower: new RegExp('^' + escapeRegexp(query.toLowerCase())) - }, { - limit: limit, - skip: offset - }); - - if (users.length < limit) { - const otherUsers = await User - .find({ - host: { $ne: null }, - usernameLower: new RegExp('^' + escapeRegexp(query.toLowerCase())) - }, { - limit: limit - users.length - }); - - users = users.concat(otherUsers); - } - - if (users.length < limit) { - const otherUsers = await User - .find({ - _id: { $nin: users.map(u => u._id) }, - host: null, - usernameLower: new RegExp(escapeRegexp(query.toLowerCase())) - }, { - limit: limit - users.length - }); - - users = users.concat(otherUsers); - } - - if (users.length < limit) { - const otherUsers = await User - .find({ - _id: { $nin: users.map(u => u._id) }, - host: { $ne: null }, - usernameLower: new RegExp(escapeRegexp(query.toLowerCase())) - }, { - limit: limit - users.length - }); - - users = users.concat(otherUsers); - } - - // Serialize - res(await Promise.all(users.map(user => pack(user, me, { detail: true })))); -}); diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts index 9024740a9..c8d588eaa 100644 --- a/src/server/api/service/github.ts +++ b/src/server/api/service/github.ts @@ -11,7 +11,7 @@ const handler = new EventEmitter(); let bot: IUser; -const post = async (text: string) => { +const post = async (text: string, home = true) => { if (bot == null) { const account = await User.findOne({ usernameLower: config.github_bot.username.toLowerCase() @@ -25,7 +25,7 @@ const post = async (text: string) => { } } - createNote(bot, { text, visibility: 'home' }); + createNote(bot, { text, visibility: home ? 'home' : 'public' }); }; // Init router @@ -130,7 +130,7 @@ handler.on('issue_comment', event => { handler.on('watch', event => { const sender = event.sender; - post(`⭐️ Starred by **${sender.login}** ⭐️`); + post(`(((⭐️))) Starred by **${sender.login}** (((⭐️)))`, false); }); handler.on('fork', event => { diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index a04bab9db..701d54777 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -294,16 +294,35 @@ export default async function( metadata.uri = uri; } - const driveFile = isLink - ? await DriveFile.insert({ - length: 0, - uploadDate: new Date(), - md5: hash, - filename: detectedName, - metadata: metadata, - contentType: mime - }) - : await (save(fs.createReadStream(path), detectedName, mime, hash, size, metadata)); + let driveFile: IDriveFile; + + if (isLink) { + try { + driveFile = await DriveFile.insert({ + length: 0, + uploadDate: new Date(), + md5: hash, + filename: detectedName, + metadata: metadata, + contentType: mime + }); + } catch (e) { + // duplicate key error (when already registered) + if (e.code === 11000) { + log(`already registered ${metadata.uri}`); + + driveFile = await DriveFile.findOne({ + 'metadata.uri': metadata.uri, + 'metadata.userId': user._id + }); + } else { + console.error(e); + throw e; + } + } + } else { + driveFile = await (save(fs.createReadStream(path), detectedName, mime, hash, size, metadata)); + } log(`drive file has been created ${driveFile._id}`); diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 20dfc78c0..4f90a19f2 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -103,6 +103,28 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< data.visibleUsers = data.visibleUsers.filter(x => x != null); } + if (data.reply && data.reply.deletedAt != null) { + return rej(); + } + + if (data.renote && data.renote.deletedAt != null) { + return rej(); + } + + // リプライ先が自分以外の非公開の投稿なら禁止 + if (data.reply && data.reply.visibility == 'private' && !data.reply.userId.equals(user._id)) { + return rej(); + } + + // Renote先が自分以外の非公開の投稿なら禁止 + if (data.renote && data.renote.visibility == 'private' && !data.renote.userId.equals(user._id)) { + return rej(); + } + + if (data.text) { + data.text = data.text.trim(); + } + // Parse MFM const tokens = data.text ? parse(data.text) : []; diff --git a/test/mfm.ts b/test/mfm.ts index df0f0be04..706c4c549 100644 --- a/test/mfm.ts +++ b/test/mfm.ts @@ -31,6 +31,28 @@ describe('Text', () => { ], tokens); }); + it('big', () => { + const tokens = analyze('***Strawberry*** Pasta'); + assert.deepEqual([ + { type: 'big', content: '***Strawberry***', big: 'Strawberry' }, + { type: 'text', content: ' Pasta' } + ], tokens); + }); + + it('motion', () => { + const tokens1 = analyze('(((Strawberry))) Pasta'); + assert.deepEqual([ + { type: 'motion', content: '(((Strawberry)))', motion: 'Strawberry' }, + { type: 'text', content: ' Pasta' } + ], tokens1); + + const tokens2 = analyze('<motion>Strawberry</motion> Pasta'); + assert.deepEqual([ + { type: 'motion', content: '<motion>Strawberry</motion>', motion: 'Strawberry' }, + { type: 'text', content: ' Pasta' } + ], tokens2); + }); + it('mention', () => { const tokens = analyze('@himawari お腹ペコい'); assert.deepEqual([