diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b1fe0be4d2..e0fb5087af 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1225,6 +1225,8 @@ common/views/components/mute-and-block.vue: word-mute: "ワードミュート" muted-words: "ミュートされたキーワード" muted-words-description: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります" + unmute-confirm: "このユーザーをミュート解除しますか?" + unblock-confirm: "このユーザーをブロック解除しますか?" save: "保存" common/views/components/password-settings.vue: diff --git a/src/client/app/common/views/components/settings/mute-and-block.user.vue b/src/client/app/common/views/components/settings/mute-and-block.user.vue new file mode 100644 index 0000000000..29ef1f7a67 --- /dev/null +++ b/src/client/app/common/views/components/settings/mute-and-block.user.vue @@ -0,0 +1,39 @@ +<template> +<div class="muteblockuser"> + <div class="avatar-link"> + <a :href="user | userPage(null, true)"> + <mk-avatar class="avatar" :user="user" :disable-link="true"/> + </a> + </div> + <div class="text"> + <div><mk-user-name :user="user"/></div> + <div class="username">@{{ user | acct }}</div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../../i18n'; + +export default Vue.extend({ + i18n: i18n('common/views/components/mute-and-block.user.vue'), + props: ['user'], +}); +</script> + +<style lang="stylus" scoped> +.muteblockuser + display flex + padding 16px + + > .avatar-link + > a + > .avatar + width 40px + height 40px + + > .text + color var(--text) + margin-left 16px +</style> diff --git a/src/client/app/common/views/components/settings/mute-and-block.vue b/src/client/app/common/views/components/settings/mute-and-block.vue index 33b19582b7..8ff5804168 100644 --- a/src/client/app/common/views/components/settings/mute-and-block.vue +++ b/src/client/app/common/views/components/settings/mute-and-block.vue @@ -6,9 +6,13 @@ <header>{{ $t('mute') }}</header> <ui-info v-if="!muteFetching && mute.length == 0">{{ $t('no-muted-users') }}</ui-info> <div class="users" v-if="mute.length != 0"> - <div v-for="user in mute" :key="user.id"> - <p><b><mk-user-name :user="user"/></b> @{{ user | acct }}</p> + <div class="user" v-for="user in mute" :key="user.id"> + <x-user :user="user"/> + <span @click="unmute(user)"> + <fa icon="times"/> + </span> </div> + <ui-button v-if="this.muteCursor != null" @click="updateMute()">{{ $t('@.load-more') }}</ui-button> </div> </section> @@ -16,9 +20,13 @@ <header>{{ $t('block') }}</header> <ui-info v-if="!blockFetching && block.length == 0">{{ $t('no-blocked-users') }}</ui-info> <div class="users" v-if="block.length != 0"> - <div v-for="user in block" :key="user.id"> - <p><b><mk-user-name :user="user"/></b> @{{ user | acct }}</p> + <div class="user" v-for="user in block" :key="user.id"> + <x-user :user="user"/> + <span @click="unblock(user)"> + <fa icon="times"/> + </span> </div> + <ui-button v-if="this.blockCursor != null" @click="updateBlock()">{{ $t('@.load-more') }}</ui-button> </div> </section> @@ -35,16 +43,25 @@ <script lang="ts"> import Vue from 'vue'; import i18n from '../../../../i18n'; +import XUser from './mute-and-block.user.vue'; + +const fetchLimit = 30; export default Vue.extend({ i18n: i18n('common/views/components/mute-and-block.vue'), + components: { + XUser + }, + data() { return { muteFetching: true, blockFetching: true, mute: [], block: [], + muteCursor: undefined, + blockCursor: undefined, mutedWords: '' }; }, @@ -59,21 +76,106 @@ export default Vue.extend({ mounted() { this.mutedWords = this._mutedWords.map(words => words.join(' ')).join('\n'); - this.$root.api('mute/list').then(mute => { - this.mute = mute.map(x => x.mutee); - this.muteFetching = false; - }); - - this.$root.api('blocking/list').then(blocking => { - this.block = blocking.map(x => x.blockee); - this.blockFetching = false; - }); + this.updateMute(); + this.updateBlock(); }, methods: { save() { this._mutedWords = this.mutedWords.split('\n').map(line => line.split(' ').filter(x => x != '')); - } + }, + + unmute(user) { + this.$root.dialog({ + type: 'warning', + text: this.$t('unmute-confirm'), + showCancelButton: true + }).then(({ canceled }) => { + if (canceled) return; + this.$root.api('mute/delete', { + userId: user.id + }).then(() => { + this.muteCursor = undefined; + this.updateMute(); + }); + }); + }, + + unblock(user) { + this.$root.dialog({ + type: 'warning', + text: this.$t('unblock-confirm'), + showCancelButton: true + }).then(({ canceled }) => { + if (canceled) return; + this.$root.api('blocking/delete', { + userId: user.id + }).then(() => { + this.updateBlock(); + }); + }); + }, + + updateMute() { + this.muteFetching = true; + this.$root.api('mute/list', { + limit: fetchLimit + 1, + untilId: this.muteCursor, + }).then((items: Object[]) => { + const past = this.muteCursor ? this.mute : []; + + if (items.length === fetchLimit + 1) { + items.pop() + this.muteCursor = items[items.length - 1].id; + } else { + this.muteCursor = undefined; + } + + this.mute = past.concat(items.map(x => x.mutee)); + this.muteFetching = false; + }); + }, + + updateBlock() { + this.blockFetching = true; + this.$root.api('blocking/list', { + limit: fetchLimit + 1, + untilId: this.blockCursor, + }).then((items: Object[]) => { + const past = this.blockCursor ? this.block : []; + + if (items.length === fetchLimit + 1) { + items.pop() + this.blockCursor = items[items.length - 1].id; + } else { + this.blockCursor = undefined; + } + + this.block = past.concat(items.map(x => x.blockee)); + this.blockFetching = false; + }); + }, } }); </script> + +<style lang="stylus" scoped> + .users + > .user + display flex + align-items center + justify-content flex-end + border-radius 6px + + &:hover + background-color var(--primary) + + > span + margin-left auto + cursor pointer + padding 16px + + > button + margin-top 16px +</style> +