parent
ac5d798cde
commit
048b9c295e
12 changed files with 486 additions and 212 deletions
|
@ -380,6 +380,19 @@ common/views/components/note-menu.vue:
|
||||||
delete-confirm: "この投稿を削除しますか?"
|
delete-confirm: "この投稿を削除しますか?"
|
||||||
remote: "投稿元で見る"
|
remote: "投稿元で見る"
|
||||||
|
|
||||||
|
common/views/components/user-menu.vue:
|
||||||
|
mention: "メンション"
|
||||||
|
mute: "ミュート"
|
||||||
|
unmute: "ミュート解除"
|
||||||
|
block: "ブロック"
|
||||||
|
unblock: "ブロック解除"
|
||||||
|
push-to-list: "リストに追加"
|
||||||
|
select-list: "リストを選択してください"
|
||||||
|
list-pushed: "{user}を{list}に追加しました"
|
||||||
|
report-abuse: "スパムを報告"
|
||||||
|
report-abuse-detail: "どのような迷惑行為を行っていますか?"
|
||||||
|
report-abuse-reported: "管理者に報告されました。ご協力ありがとうございました。"
|
||||||
|
|
||||||
common/views/components/poll.vue:
|
common/views/components/poll.vue:
|
||||||
vote-to: "「{}」に投票する"
|
vote-to: "「{}」に投票する"
|
||||||
vote-count: "{}票"
|
vote-count: "{}票"
|
||||||
|
@ -1103,6 +1116,7 @@ admin/views/index.vue:
|
||||||
federation: "連合"
|
federation: "連合"
|
||||||
announcements: "お知らせ"
|
announcements: "お知らせ"
|
||||||
hashtags: "ハッシュタグ"
|
hashtags: "ハッシュタグ"
|
||||||
|
abuse: "スパム報告"
|
||||||
back-to-misskey: "Misskeyに戻る"
|
back-to-misskey: "Misskeyに戻る"
|
||||||
|
|
||||||
admin/views/dashboard.vue:
|
admin/views/dashboard.vue:
|
||||||
|
@ -1114,6 +1128,13 @@ admin/views/dashboard.vue:
|
||||||
this-instance: "このインスタンス"
|
this-instance: "このインスタンス"
|
||||||
federated: "連合"
|
federated: "連合"
|
||||||
|
|
||||||
|
admin/views/abuse.vue:
|
||||||
|
title: "スパム報告"
|
||||||
|
target: "対象"
|
||||||
|
reporter: "報告者"
|
||||||
|
details: "詳細"
|
||||||
|
remove-report: "削除"
|
||||||
|
|
||||||
admin/views/instance.vue:
|
admin/views/instance.vue:
|
||||||
instance: "インスタンス"
|
instance: "インスタンス"
|
||||||
instance-name: "インスタンス名"
|
instance-name: "インスタンス名"
|
||||||
|
@ -1384,20 +1405,12 @@ desktop/views/pages/user/user.profile.vue:
|
||||||
stalk: "ストークする"
|
stalk: "ストークする"
|
||||||
stalking: "ストーキングしています"
|
stalking: "ストーキングしています"
|
||||||
unstalk: "ストーク解除"
|
unstalk: "ストーク解除"
|
||||||
mute: "ミュートする"
|
menu: "メニュー"
|
||||||
muted: "ミュートしています"
|
|
||||||
unmute: "ミュート解除"
|
|
||||||
block: "ブロックする"
|
|
||||||
unblock: "ブロック解除"
|
|
||||||
block-confirm: "このユーザーをブロックしますか?"
|
|
||||||
push-to-a-list: "リストに追加"
|
|
||||||
list-pushed: "{user}を{list}に追加しました。"
|
|
||||||
|
|
||||||
desktop/views/pages/user/user.header.vue:
|
desktop/views/pages/user/user.header.vue:
|
||||||
posts: "投稿"
|
posts: "投稿"
|
||||||
following: "フォロー"
|
following: "フォロー"
|
||||||
followers: "フォロワー"
|
followers: "フォロワー"
|
||||||
mention: "メンション"
|
|
||||||
is-bot: "このアカウントはBotです"
|
is-bot: "このアカウントはBotです"
|
||||||
years-old: "{age}歳"
|
years-old: "{age}歳"
|
||||||
year: "年"
|
year: "年"
|
||||||
|
@ -1686,14 +1699,7 @@ mobile/views/pages/user.vue:
|
||||||
overview: "概要"
|
overview: "概要"
|
||||||
timeline: "タイムライン"
|
timeline: "タイムライン"
|
||||||
media: "メディア"
|
media: "メディア"
|
||||||
mute: "ミュート"
|
|
||||||
unmute: "ミュート解除"
|
|
||||||
block: "ブロック"
|
|
||||||
unblock: "ブロック解除"
|
|
||||||
years-old: "{age}歳"
|
years-old: "{age}歳"
|
||||||
push-to-list: "リストに追加"
|
|
||||||
select-list: "リストを選択してください"
|
|
||||||
list-pushed: "{user}を{list}に追加しました"
|
|
||||||
|
|
||||||
mobile/views/pages/user/home.vue:
|
mobile/views/pages/user/home.vue:
|
||||||
recent-notes: "最近の投稿"
|
recent-notes: "最近の投稿"
|
||||||
|
@ -1747,12 +1753,10 @@ deck/deck.user-column.vue:
|
||||||
posts: "投稿"
|
posts: "投稿"
|
||||||
following: "フォロー"
|
following: "フォロー"
|
||||||
followers: "フォロワー"
|
followers: "フォロワー"
|
||||||
mention: "メンション"
|
|
||||||
images: "画像"
|
images: "画像"
|
||||||
activity: "アクティビティ"
|
activity: "アクティビティ"
|
||||||
timeline: "タイムライン"
|
timeline: "タイムライン"
|
||||||
pinned-notes: "ピン留めされた投稿"
|
pinned-notes: "ピン留めされた投稿"
|
||||||
push-to-a-list: "リストに追加"
|
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
edit-this-page-on-github: "間違いや改善点を見つけましたか?"
|
edit-this-page-on-github: "間違いや改善点を見つけましたか?"
|
||||||
|
|
87
src/client/app/admin/views/abuse.vue
Normal file
87
src/client/app/admin/views/abuse.vue
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<div class="wbjusose">
|
||||||
|
<ui-card>
|
||||||
|
<div slot="title"><fa :icon="faExclamationCircle"/> {{ $t('title') }}</div>
|
||||||
|
<section class="fit-top">
|
||||||
|
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||||
|
<div v-for="report in userReports" :key="report.id" class="haexwsjc">
|
||||||
|
<ui-horizon-group inputs>
|
||||||
|
<ui-input :value="report.user | acct" type="text">
|
||||||
|
<span>{{ $t('target') }}</span>
|
||||||
|
</ui-input>
|
||||||
|
<ui-input :value="report.reporter | acct" type="text">
|
||||||
|
<span>{{ $t('reporter') }}</span>
|
||||||
|
</ui-input>
|
||||||
|
</ui-horizon-group>
|
||||||
|
<ui-textarea :value="report.comment" readonly>
|
||||||
|
<span>{{ $t('details') }}</span>
|
||||||
|
</ui-textarea>
|
||||||
|
<ui-button @click="removeReport(report)">{{ $t('remove-report') }}</ui-button>
|
||||||
|
</div>
|
||||||
|
</sequential-entrance>
|
||||||
|
<ui-button v-if="existMore" @click="fetchUserReports">{{ $t('@.load-more') }}</ui-button>
|
||||||
|
</section>
|
||||||
|
</ui-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import i18n from '../../i18n';
|
||||||
|
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
i18n: i18n('admin/views/abuse.vue'),
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
limit: 10,
|
||||||
|
untilId: undefined,
|
||||||
|
userReports: [],
|
||||||
|
existMore: false,
|
||||||
|
faExclamationCircle
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.fetchUserReports();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
fetchUserReports() {
|
||||||
|
this.$root.api('admin/abuse-user-reports', {
|
||||||
|
untilId: this.untilId,
|
||||||
|
limit: this.limit + 1
|
||||||
|
}).then(reports => {
|
||||||
|
if (reports.length == this.limit + 1) {
|
||||||
|
reports.pop();
|
||||||
|
this.existMore = true;
|
||||||
|
} else {
|
||||||
|
this.existMore = false;
|
||||||
|
}
|
||||||
|
this.userReports = this.userReports.concat(reports);
|
||||||
|
this.untilId = this.userReports[this.userReports.length - 1].id;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeReport(report) {
|
||||||
|
this.$root.api('admin/remove-abuse-user-report', {
|
||||||
|
reportId: report.id
|
||||||
|
}).then(() => {
|
||||||
|
this.userReports = this.userReports.filter(r => r.id != report.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.wbjusose
|
||||||
|
@media (min-width 500px)
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
.haexwsjc
|
||||||
|
padding-bottom 16px
|
||||||
|
border-bottom solid 1px var(--faceDivider)
|
||||||
|
|
||||||
|
</style>
|
|
@ -27,6 +27,7 @@
|
||||||
<li @click="nav('emoji')" :class="{ active: page == 'emoji' }"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</li>
|
<li @click="nav('emoji')" :class="{ active: page == 'emoji' }"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</li>
|
||||||
<li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</li>
|
<li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</li>
|
||||||
<li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }"><fa icon="hashtag" fixed-width/>{{ $t('hashtags') }}</li>
|
<li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }"><fa icon="hashtag" fixed-width/>{{ $t('hashtags') }}</li>
|
||||||
|
<li @click="nav('abuse')" :class="{ active: page == 'abuse' }"><fa :icon="faExclamationCircle" fixed-width/>{{ $t('abuse') }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="back-to-misskey">
|
<div class="back-to-misskey">
|
||||||
<a href="/"><fa :icon="faArrowLeft"/> {{ $t('back-to-misskey') }}</a>
|
<a href="/"><fa :icon="faArrowLeft"/> {{ $t('back-to-misskey') }}</a>
|
||||||
|
@ -45,7 +46,7 @@
|
||||||
<div v-if="page == 'announcements'"><x-announcements/></div>
|
<div v-if="page == 'announcements'"><x-announcements/></div>
|
||||||
<div v-if="page == 'hashtags'"><x-hashtags/></div>
|
<div v-if="page == 'hashtags'"><x-hashtags/></div>
|
||||||
<div v-if="page == 'drive'"><x-drive/></div>
|
<div v-if="page == 'drive'"><x-drive/></div>
|
||||||
<div v-if="page == 'update'"></div>
|
<div v-if="page == 'abuse'"><x-abuse/></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -63,7 +64,8 @@ import XAnnouncements from "./announcements.vue";
|
||||||
import XHashtags from "./hashtags.vue";
|
import XHashtags from "./hashtags.vue";
|
||||||
import XUsers from "./users.vue";
|
import XUsers from "./users.vue";
|
||||||
import XDrive from "./drive.vue";
|
import XDrive from "./drive.vue";
|
||||||
import { faHeadset, faArrowLeft, faShareAlt } from '@fortawesome/free-solid-svg-icons';
|
import XAbuse from "./abuse.vue";
|
||||||
|
import { faHeadset, faArrowLeft, faShareAlt, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faGrin } from '@fortawesome/free-regular-svg-icons';
|
import { faGrin } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
|
||||||
// Detect the user agent
|
// Detect the user agent
|
||||||
|
@ -81,6 +83,7 @@ export default Vue.extend({
|
||||||
XHashtags,
|
XHashtags,
|
||||||
XUsers,
|
XUsers,
|
||||||
XDrive,
|
XDrive,
|
||||||
|
XAbuse,
|
||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
isMobile
|
isMobile
|
||||||
|
@ -94,7 +97,8 @@ export default Vue.extend({
|
||||||
faGrin,
|
faGrin,
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faHeadset,
|
faHeadset,
|
||||||
faShareAlt
|
faShareAlt,
|
||||||
|
faExclamationCircle
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
157
src/client/app/common/views/components/user-menu.vue
Normal file
157
src/client/app/common/views/components/user-menu.vue
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
<template>
|
||||||
|
<div style="position:initial">
|
||||||
|
<mk-menu :source="source" :items="items" @closed="closed"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import i18n from '../../../i18n';
|
||||||
|
import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
|
||||||
|
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
i18n: i18n('common/views/components/user-menu.vue'),
|
||||||
|
|
||||||
|
props: ['user', 'source'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let menu = [{
|
||||||
|
icon: ['fas', 'at'],
|
||||||
|
text: this.$t('mention'),
|
||||||
|
action: () => {
|
||||||
|
this.$post({ mention: this.user });
|
||||||
|
}
|
||||||
|
}, null, {
|
||||||
|
icon: ['fas', 'list'],
|
||||||
|
text: this.$t('push-to-list'),
|
||||||
|
action: this.pushList
|
||||||
|
}, null, {
|
||||||
|
icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'],
|
||||||
|
text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'),
|
||||||
|
action: this.toggleMute
|
||||||
|
}, {
|
||||||
|
icon: 'ban',
|
||||||
|
text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'),
|
||||||
|
action: this.toggleBlock
|
||||||
|
}, null, {
|
||||||
|
icon: faExclamationCircle,
|
||||||
|
text: this.$t('report-abuse'),
|
||||||
|
action: this.reportAbuse
|
||||||
|
}];
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: menu
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
closed() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.destroyDom();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async pushList() {
|
||||||
|
const lists = await this.$root.api('users/lists/list');
|
||||||
|
const { canceled, result: listId } = await this.$root.dialog({
|
||||||
|
type: null,
|
||||||
|
title: this.$t('select-list'),
|
||||||
|
select: {
|
||||||
|
items: lists.map(list => ({
|
||||||
|
value: list.id, text: list.title
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
showCancelButton: true
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
await this.$root.api('users/lists/push', {
|
||||||
|
listId: listId,
|
||||||
|
userId: this.user.id
|
||||||
|
});
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'success',
|
||||||
|
text: this.$t('list-pushed', {
|
||||||
|
user: this.user.name,
|
||||||
|
list: lists.find(l => l.id === listId).title
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMute() {
|
||||||
|
if (this.user.isMuted) {
|
||||||
|
this.$root.api('mute/delete', {
|
||||||
|
userId: this.user.id
|
||||||
|
}).then(() => {
|
||||||
|
this.user.isMuted = false;
|
||||||
|
}, () => {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$root.api('mute/create', {
|
||||||
|
userId: this.user.id
|
||||||
|
}).then(() => {
|
||||||
|
this.user.isMuted = true;
|
||||||
|
}, () => {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleBlock() {
|
||||||
|
if (this.user.isBlocking) {
|
||||||
|
this.$root.api('blocking/delete', {
|
||||||
|
userId: this.user.id
|
||||||
|
}).then(() => {
|
||||||
|
this.user.isBlocking = false;
|
||||||
|
}, () => {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$root.api('blocking/create', {
|
||||||
|
userId: this.user.id
|
||||||
|
}).then(() => {
|
||||||
|
this.user.isBlocking = true;
|
||||||
|
}, () => {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async reportAbuse() {
|
||||||
|
const reported = this.$t('report-abuse-reported'); // なぜか後で参照すると null になるので最初にメモリに確保しておく
|
||||||
|
const { canceled, result: comment } = await this.$root.dialog({
|
||||||
|
title: this.$t('report-abuse-detail'),
|
||||||
|
input: true
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
this.$root.api('users/report-abuse', {
|
||||||
|
userId: this.user.id,
|
||||||
|
comment: comment
|
||||||
|
}).then(() => {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'success',
|
||||||
|
text: reported
|
||||||
|
});
|
||||||
|
}, e => {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -49,9 +49,6 @@
|
||||||
<b>{{ user.followersCount | number }}</b>
|
<b>{{ user.followersCount | number }}</b>
|
||||||
<span>{{ $t('followers') }}</span>
|
<span>{{ $t('followers') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mention">
|
|
||||||
<button @click="mention" :title="$t('mention')"><fa icon="at"/></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pinned" v-if="user.pinnedNotes && user.pinnedNotes.length > 0">
|
<div class="pinned" v-if="user.pinnedNotes && user.pinnedNotes.length > 0">
|
||||||
|
@ -100,8 +97,7 @@ import parseAcct from '../../../../../../misc/acct/parse';
|
||||||
import XColumn from './deck.column.vue';
|
import XColumn from './deck.column.vue';
|
||||||
import XNotes from './deck.notes.vue';
|
import XNotes from './deck.notes.vue';
|
||||||
import XNote from '../../components/note.vue';
|
import XNote from '../../components/note.vue';
|
||||||
import Menu from '../../../../common/views/components/menu.vue';
|
import XUserMenu from '../../../../common/views/components/user-menu.vue';
|
||||||
import MkUserListsWindow from '../../components/user-lists-window.vue';
|
|
||||||
import { concat } from '../../../../../../prelude/array';
|
import { concat } from '../../../../../../prelude/array';
|
||||||
import * as ApexCharts from 'apexcharts';
|
import * as ApexCharts from 'apexcharts';
|
||||||
|
|
||||||
|
@ -306,33 +302,10 @@ export default Vue.extend({
|
||||||
return promise;
|
return promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
mention() {
|
|
||||||
this.$post({ mention: this.user });
|
|
||||||
},
|
|
||||||
|
|
||||||
menu() {
|
menu() {
|
||||||
let menu = [{
|
this.$root.new(XUserMenu, {
|
||||||
icon: 'list',
|
|
||||||
text: this.$t('push-to-a-list'),
|
|
||||||
action: () => {
|
|
||||||
const w = this.$root.new(MkUserListsWindow);
|
|
||||||
w.$once('choosen', async list => {
|
|
||||||
w.close();
|
|
||||||
await this.$root.api('users/lists/push', {
|
|
||||||
listId: list.id,
|
|
||||||
userId: this.user.id
|
|
||||||
});
|
|
||||||
this.$root.dialog({
|
|
||||||
type: 'success',
|
|
||||||
splash: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
this.$root.new(Menu, {
|
|
||||||
source: this.$refs.menu,
|
source: this.$refs.menu,
|
||||||
items: menu
|
user: this.user
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -459,7 +432,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
> .counts
|
> .counts
|
||||||
display grid
|
display grid
|
||||||
grid-template-columns 2fr 2fr 2fr 1fr
|
grid-template-columns 2fr 2fr 2fr
|
||||||
margin-top 8px
|
margin-top 8px
|
||||||
border-top solid var(--lineWidth) var(--faceDivider)
|
border-top solid var(--lineWidth) var(--faceDivider)
|
||||||
|
|
||||||
|
@ -476,9 +449,6 @@ export default Vue.extend({
|
||||||
font-size 80%
|
font-size 80%
|
||||||
opacity 0.7
|
opacity 0.7
|
||||||
|
|
||||||
> .mention
|
|
||||||
display flex
|
|
||||||
|
|
||||||
> *
|
> *
|
||||||
> p.caption
|
> p.caption
|
||||||
margin 0
|
margin 0
|
||||||
|
|
|
@ -36,7 +36,6 @@
|
||||||
<span class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</span>
|
<span class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</span>
|
||||||
<router-link :to="user | userPage('following')" class="following clickable"><b>{{ user.followingCount | number }}</b>{{ $t('following') }}</router-link>
|
<router-link :to="user | userPage('following')" class="following clickable"><b>{{ user.followingCount | number }}</b>{{ $t('following') }}</router-link>
|
||||||
<router-link :to="user | userPage('followers')" class="followers clickable"><b>{{ user.followersCount | number }}</b>{{ $t('followers') }}</router-link>
|
<router-link :to="user | userPage('followers')" class="followers clickable"><b>{{ user.followersCount | number }}</b>{{ $t('followers') }}</router-link>
|
||||||
<button @click="mention" :title="$t('mention')"><fa icon="at"/></button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,15 +9,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-form">
|
<div class="action-form">
|
||||||
<ui-button @click="user.isMuted ? unmute() : mute()" v-if="$store.state.i.id != user.id">
|
<ui-button @click="menu" ref="menu">{{ $t('menu') }}</ui-button>
|
||||||
<span v-if="user.isMuted"><fa icon="eye"/> {{ $t('unmute') }}</span>
|
|
||||||
<span v-else><fa :icon="['far', 'eye-slash']"/> {{ $t('mute') }}</span>
|
|
||||||
</ui-button>
|
|
||||||
<ui-button @click="user.isBlocking ? unblock() : block()" v-if="$store.state.i.id != user.id">
|
|
||||||
<span v-if="user.isBlocking"><fa icon="ban"/> {{ $t('unblock') }}</span>
|
|
||||||
<span v-else><fa icon="ban"/> {{ $t('block') }}</span>
|
|
||||||
</ui-button>
|
|
||||||
<ui-button @click="list"><fa icon="list"/> {{ $t('push-to-a-list') }}</ui-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -25,7 +17,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../../../i18n';
|
import i18n from '../../../../i18n';
|
||||||
import MkUserListsWindow from '../../components/user-lists-window.vue';
|
import XUserMenu from '../../../../common/views/components/user-menu.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('desktop/views/pages/user/user.profile.vue'),
|
i18n: i18n('desktop/views/pages/user/user.profile.vue'),
|
||||||
|
@ -52,72 +44,12 @@ export default Vue.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
mute() {
|
menu() {
|
||||||
this.$root.api('mute/create', {
|
this.$root.new(XUserMenu, {
|
||||||
userId: this.user.id
|
source: this.$refs.menu.$el,
|
||||||
}).then(() => {
|
user: this.user
|
||||||
this.user.isMuted = true;
|
|
||||||
}, () => {
|
|
||||||
alert('error');
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
unmute() {
|
|
||||||
this.$root.api('mute/delete', {
|
|
||||||
userId: this.user.id
|
|
||||||
}).then(() => {
|
|
||||||
this.user.isMuted = false;
|
|
||||||
}, () => {
|
|
||||||
alert('error');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
block() {
|
|
||||||
this.$root.dialog({
|
|
||||||
type: 'warning',
|
|
||||||
text: this.$t('block-confirm'),
|
|
||||||
showCancelButton: true
|
|
||||||
}).then(({ canceled }) => {
|
|
||||||
if (canceled) return;
|
|
||||||
|
|
||||||
this.$root.api('blocking/create', {
|
|
||||||
userId: this.user.id
|
|
||||||
}).then(() => {
|
|
||||||
this.user.isBlocking = true;
|
|
||||||
}, () => {
|
|
||||||
alert('error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
unblock() {
|
|
||||||
this.$root.api('blocking/delete', {
|
|
||||||
userId: this.user.id
|
|
||||||
}).then(() => {
|
|
||||||
this.user.isBlocking = false;
|
|
||||||
}, () => {
|
|
||||||
alert('error');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
list() {
|
|
||||||
const w = this.$root.new(MkUserListsWindow);
|
|
||||||
w.$once('choosen', async list => {
|
|
||||||
w.close();
|
|
||||||
await this.$root.api('users/lists/push', {
|
|
||||||
listId: list.id,
|
|
||||||
userId: this.user.id
|
|
||||||
});
|
|
||||||
this.$root.dialog({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Done!',
|
|
||||||
text: this.$t('list-pushed', {
|
|
||||||
user: this.user.name,
|
|
||||||
list: list.title
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -55,7 +55,6 @@
|
||||||
<b>{{ user.followersCount | number }}</b>
|
<b>{{ user.followersCount | number }}</b>
|
||||||
<i>{{ $t('followers') }}</i>
|
<i>{{ $t('followers') }}</i>
|
||||||
</a>
|
</a>
|
||||||
<button @click="mention"><fa icon="at"/></button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -81,7 +80,7 @@ import i18n from '../../../i18n';
|
||||||
import * as age from 's-age';
|
import * as age from 's-age';
|
||||||
import parseAcct from '../../../../../misc/acct/parse';
|
import parseAcct from '../../../../../misc/acct/parse';
|
||||||
import Progress from '../../../common/scripts/loading';
|
import Progress from '../../../common/scripts/loading';
|
||||||
import Menu from '../../../common/views/components/menu.vue';
|
import XUserMenu from '../../../common/views/components/user-menu.vue';
|
||||||
import XHome from './user/home.vue';
|
import XHome from './user/home.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
@ -127,88 +126,10 @@ export default Vue.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
mention() {
|
|
||||||
this.$post({ mention: this.user });
|
|
||||||
},
|
|
||||||
|
|
||||||
menu() {
|
menu() {
|
||||||
let menu = [{
|
this.$root.new(XUserMenu, {
|
||||||
icon: ['fas', 'list'],
|
|
||||||
text: this.$t('push-to-list'),
|
|
||||||
action: async () => {
|
|
||||||
const lists = await this.$root.api('users/lists/list');
|
|
||||||
const { canceled, result: listId } = await this.$root.dialog({
|
|
||||||
type: null,
|
|
||||||
title: this.$t('select-list'),
|
|
||||||
select: {
|
|
||||||
items: lists.map(list => ({
|
|
||||||
value: list.id, text: list.title
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
showCancelButton: true
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
await this.$root.api('users/lists/push', {
|
|
||||||
listId: listId,
|
|
||||||
userId: this.user.id
|
|
||||||
});
|
|
||||||
this.$root.dialog({
|
|
||||||
type: 'success',
|
|
||||||
text: this.$t('list-pushed', {
|
|
||||||
user: this.user.name,
|
|
||||||
list: lists.find(l => l.id === listId).title
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, null, {
|
|
||||||
icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'],
|
|
||||||
text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'),
|
|
||||||
action: () => {
|
|
||||||
if (this.user.isMuted) {
|
|
||||||
this.$root.api('mute/delete', {
|
|
||||||
userId: this.user.id
|
|
||||||
}).then(() => {
|
|
||||||
this.user.isMuted = false;
|
|
||||||
}, () => {
|
|
||||||
alert('error');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.$root.api('mute/create', {
|
|
||||||
userId: this.user.id
|
|
||||||
}).then(() => {
|
|
||||||
this.user.isMuted = true;
|
|
||||||
}, () => {
|
|
||||||
alert('error');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
icon: 'ban',
|
|
||||||
text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'),
|
|
||||||
action: () => {
|
|
||||||
if (this.user.isBlocking) {
|
|
||||||
this.$root.api('blocking/delete', {
|
|
||||||
userId: this.user.id
|
|
||||||
}).then(() => {
|
|
||||||
this.user.isBlocking = false;
|
|
||||||
}, () => {
|
|
||||||
alert('error');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.$root.api('blocking/create', {
|
|
||||||
userId: this.user.id
|
|
||||||
}).then(() => {
|
|
||||||
this.user.isBlocking = true;
|
|
||||||
}, () => {
|
|
||||||
alert('error');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
this.$root.new(Menu, {
|
|
||||||
source: this.$refs.menu,
|
source: this.$refs.menu,
|
||||||
items: menu
|
user: this.user
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
52
src/models/abuse-user-report.ts
Normal file
52
src/models/abuse-user-report.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import * as mongo from 'mongodb';
|
||||||
|
const deepcopy = require('deepcopy');
|
||||||
|
import db from '../db/mongodb';
|
||||||
|
import isObjectId from '../misc/is-objectid';
|
||||||
|
import { pack as packUser } from './user';
|
||||||
|
|
||||||
|
const AbuseUserReport = db.get<IAbuseUserReport>('abuseUserReports');
|
||||||
|
AbuseUserReport.createIndex('userId');
|
||||||
|
AbuseUserReport.createIndex('reporterId');
|
||||||
|
AbuseUserReport.createIndex(['userId', 'reporterId'], { unique: true });
|
||||||
|
export default AbuseUserReport;
|
||||||
|
|
||||||
|
export interface IAbuseUserReport {
|
||||||
|
_id: mongo.ObjectID;
|
||||||
|
createdAt: Date;
|
||||||
|
userId: mongo.ObjectID;
|
||||||
|
reporterId: mongo.ObjectID;
|
||||||
|
comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const packMany = (
|
||||||
|
reports: (string | mongo.ObjectID | IAbuseUserReport)[]
|
||||||
|
) => {
|
||||||
|
return Promise.all(reports.map(x => pack(x)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pack = (
|
||||||
|
report: any
|
||||||
|
) => new Promise<any>(async (resolve, reject) => {
|
||||||
|
let _report: any;
|
||||||
|
|
||||||
|
if (isObjectId(report)) {
|
||||||
|
_report = await AbuseUserReport.findOne({
|
||||||
|
_id: report
|
||||||
|
});
|
||||||
|
} else if (typeof report === 'string') {
|
||||||
|
_report = await AbuseUserReport.findOne({
|
||||||
|
_id: new mongo.ObjectID(report)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_report = deepcopy(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename _id to id
|
||||||
|
_report.id = _report._id;
|
||||||
|
delete _report._id;
|
||||||
|
|
||||||
|
_report.reporter = await packUser(_report.reporterId, null, { detail: true });
|
||||||
|
_report.user = await packUser(_report.userId, null, { detail: true });
|
||||||
|
|
||||||
|
resolve(_report);
|
||||||
|
});
|
54
src/server/api/endpoints/admin/abuse-user-reports.ts
Normal file
54
src/server/api/endpoints/admin/abuse-user-reports.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
|
||||||
|
import Report, { packMany } from '../../../../models/abuse-user-report';
|
||||||
|
import define from '../../define';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
limit: {
|
||||||
|
validator: $.num.optional.range(1, 100),
|
||||||
|
default: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
sinceId: {
|
||||||
|
validator: $.type(ID).optional,
|
||||||
|
transform: transform,
|
||||||
|
},
|
||||||
|
|
||||||
|
untilId: {
|
||||||
|
validator: $.type(ID).optional,
|
||||||
|
transform: transform,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, (ps) => new Promise(async (res, rej) => {
|
||||||
|
if (ps.sinceId && ps.untilId) {
|
||||||
|
return rej('cannot set sinceId and untilId');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sort = {
|
||||||
|
_id: -1
|
||||||
|
};
|
||||||
|
const query = {} as any;
|
||||||
|
if (ps.sinceId) {
|
||||||
|
sort._id = 1;
|
||||||
|
query._id = {
|
||||||
|
$gt: ps.sinceId
|
||||||
|
};
|
||||||
|
} else if (ps.untilId) {
|
||||||
|
query._id = {
|
||||||
|
$lt: ps.untilId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const reports = await Report
|
||||||
|
.find(query, {
|
||||||
|
limit: ps.limit,
|
||||||
|
sort: sort
|
||||||
|
});
|
||||||
|
|
||||||
|
res(await packMany(reports));
|
||||||
|
}));
|
32
src/server/api/endpoints/admin/remove-abuse-user-report.ts
Normal file
32
src/server/api/endpoints/admin/remove-abuse-user-report.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import ID, { transform } from '../../../../misc/cafy-id';
|
||||||
|
import define from '../../define';
|
||||||
|
import AbuseUserReport from '../../../../models/abuse-user-report';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
reportId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
transform: transform
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, (ps) => new Promise(async (res, rej) => {
|
||||||
|
const report = await AbuseUserReport.findOne({
|
||||||
|
_id: ps.reportId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (report == null) {
|
||||||
|
return rej('report not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await AbuseUserReport.remove({
|
||||||
|
_id: report._id
|
||||||
|
});
|
||||||
|
|
||||||
|
res();
|
||||||
|
}));
|
62
src/server/api/endpoints/users/report-abuse.ts
Normal file
62
src/server/api/endpoints/users/report-abuse.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
|
||||||
|
import define from '../../define';
|
||||||
|
import User from '../../../../models/user';
|
||||||
|
import AbuseUserReport from '../../../../models/abuse-user-report';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '指定したユーザーを迷惑なユーザーであると報告します。'
|
||||||
|
},
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
userId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
transform: transform,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '対象のユーザーのID',
|
||||||
|
'en-US': 'Target user ID'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
comment: {
|
||||||
|
validator: $.str.range(1, 3000),
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '迷惑行為の詳細'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
||||||
|
// Lookup user
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: ps.userId
|
||||||
|
}, {
|
||||||
|
fields: {
|
||||||
|
_id: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
return rej('user not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user._id.equals(me._id)) {
|
||||||
|
return rej('cannot report yourself');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isAdmin) {
|
||||||
|
return rej('cannot report admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
await AbuseUserReport.insert({
|
||||||
|
createdAt: new Date(),
|
||||||
|
userId: user._id,
|
||||||
|
reporterId: me._id,
|
||||||
|
comment: ps.comment
|
||||||
|
});
|
||||||
|
|
||||||
|
res();
|
||||||
|
}));
|
Loading…
Reference in a new issue