Merge pull request #3047 from mei23/mei-1031-blokings-list

blockings list
This commit is contained in:
syuilo 2018-10-31 11:17:24 +09:00 committed by GitHub
commit 5e3372e932
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 254 additions and 89 deletions

View File

@ -832,7 +832,8 @@ desktop/views/components/settings.vue:
profile: "プロフィール" profile: "プロフィール"
notification: "通知" notification: "通知"
apps: "アプリ" apps: "アプリ"
mute: "ミュート" mute-and-block: "ミュート/ブロック"
blocking: "ブロック"
security: "セキュリティ" security: "セキュリティ"
signin: "サインイン履歴" signin: "サインイン履歴"
password: "パスワード" password: "パスワード"
@ -973,8 +974,12 @@ common/views/components/drive-settings.vue:
in-use: "使用中" in-use: "使用中"
stats: "統計" stats: "統計"
desktop/views/components/settings.mute.vue: common/views/components/mute-and-block.vue:
no-users: "ミュートしているユーザーはいません" mute-and-block: "ミュートとブロック"
mute: "ミュート"
block: "ブロック"
no-muted-users: "ミュートしているユーザーはいません"
no-blocked-users: "ブロックしているユーザーはいません"
desktop/views/components/settings.password.vue: desktop/views/components/settings.password.vue:
reset: "パスワードを変更する" reset: "パスワードを変更する"

View File

@ -1,5 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import muteAndBlock from './mute-and-block.vue';
import error from './error.vue'; import error from './error.vue';
import apiSettings from './api-settings.vue'; import apiSettings from './api-settings.vue';
import driveSettings from './drive-settings.vue'; import driveSettings from './drive-settings.vue';
@ -50,6 +51,7 @@ import uiInfo from './ui/info.vue';
import formButton from './ui/form/button.vue'; import formButton from './ui/form/button.vue';
import formRadio from './ui/form/radio.vue'; import formRadio from './ui/form/radio.vue';
Vue.component('mk-mute-and-block', muteAndBlock);
Vue.component('mk-error', error); Vue.component('mk-error', error);
Vue.component('mk-api-settings', apiSettings); Vue.component('mk-api-settings', apiSettings);
Vue.component('mk-drive-settings', driveSettings); Vue.component('mk-drive-settings', driveSettings);

View File

@ -0,0 +1,56 @@
<template>
<ui-card>
<div slot="title">%fa:ban% %i18n:@mute-and-block%</div>
<section>
<header>%i18n:@mute%</header>
<ui-info v-if="!muteFetching && mute.length == 0">
<p>%i18n:@no-muted-users%</p>
</ui-info>
<div class="users" v-if="mute.length != 0">
<div v-for="user in mute" :key="user.id">
<p><b>{{ user | userName }}</b> @{{ user | acct }}</p>
</div>
</div>
</section>
<section>
<header>%i18n:@block%</header>
<ui-info v-if="!blockFetching && block.length == 0">
<p>%i18n:@no-blocked-users%</p>
</ui-info>
<div class="users" v-if="block.length != 0">
<div v-for="user in block" :key="user.id">
<p><b>{{ user | userName }}</b> @{{ user | acct }}</p>
</div>
</div>
</section>
</ui-card>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
muteFetching: true,
blockFetching: true,
mute: [],
block: []
};
},
mounted() {
(this as any).api('mute/list').then(mute => {
this.mute = mute.map(x => x.mutee);
this.muteFetching = false;
});
(this as any).api('blocking/list').then(blocking => {
this.block = blocking.map(x => x.blockee);
this.blockFetching = false;
});
}
});
</script>

View File

@ -1,31 +0,0 @@
<template>
<div>
<div class="none ui info" v-if="!fetching && users.length == 0">
<p>%fa:info-circle%%i18n:@no-users%</p>
</div>
<div class="users" v-if="users.length != 0">
<div v-for="user in users" :key="user.id">
<p><b>{{ user | userName }}</b> @{{ user | acct }}</p>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
fetching: true,
users: []
};
},
mounted() {
(this as any).api('mute/list').then(x => {
this.users = x.users;
this.fetching = false;
});
}
});
</script>

View File

@ -7,7 +7,7 @@
<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p> <p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p>
<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:common.drive%</p> <p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:common.drive%</p>
<p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'">%fa:hashtag .fw%%i18n:@tags%</p> <p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'">%fa:hashtag .fw%%i18n:@tags%</p>
<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p> <p :class="{ active: page == 'muteAndBlock' }" @mousedown="page = 'muteAndBlock'">%fa:ban .fw%%i18n:@mute-and-block%</p>
<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p> <p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p>
<p :class="{ active: page == 'security' }" @mousedown="page = 'security'">%fa:unlock-alt .fw%%i18n:@security%</p> <p :class="{ active: page == 'security' }" @mousedown="page = 'security'">%fa:unlock-alt .fw%%i18n:@security%</p>
<p :class="{ active: page == 'api' }" @mousedown="page = 'api'">%fa:key .fw%API</p> <p :class="{ active: page == 'api' }" @mousedown="page = 'api'">%fa:key .fw%API</p>
@ -200,12 +200,9 @@
</section> </section>
</ui-card> </ui-card>
<ui-card class="mute" v-show="page == 'mute'"> <div class="muteAndBlock" v-show="page == 'muteAndBlock'">
<div slot="title">%fa:ban% %i18n:@mute%</div> <mk-mute-and-block/>
<section> </div>
<x-mute/>
</section>
</ui-card>
<ui-card class="apps" v-show="page == 'apps'"> <ui-card class="apps" v-show="page == 'apps'">
<div slot="title">%fa:puzzle-piece% %i18n:@apps%</div> <div slot="title">%fa:puzzle-piece% %i18n:@apps%</div>
@ -289,7 +286,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import XMute from './settings.mute.vue';
import XPassword from './settings.password.vue'; import XPassword from './settings.password.vue';
import X2fa from './settings.2fa.vue'; import X2fa from './settings.2fa.vue';
import XApps from './settings.apps.vue'; import XApps from './settings.apps.vue';
@ -300,7 +296,6 @@ import checkForUpdate from '../../../common/scripts/check-for-update';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XMute,
XPassword, XPassword,
X2fa, X2fa,
XApps, XApps,

View File

@ -85,6 +85,8 @@
<mk-drive-settings/> <mk-drive-settings/>
<mk-mute-and-block/>
<ui-card> <ui-card>
<div slot="title">%fa:volume-up% %i18n:@sound%</div> <div slot="title">%fa:volume-up% %i18n:@sound%</div>

View File

@ -1,7 +1,12 @@
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import db from '../db/mongodb'; import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
const deepcopy = require('deepcopy');
import { pack as packUser } from './user';
const Blocking = db.get<IBlocking>('blocking'); const Blocking = db.get<IBlocking>('blocking');
Blocking.createIndex('blockerId');
Blocking.createIndex('blockeeId');
Blocking.createIndex(['blockerId', 'blockeeId'], { unique: true }); Blocking.createIndex(['blockerId', 'blockeeId'], { unique: true });
export default Blocking; export default Blocking;
@ -11,3 +16,39 @@ export type IBlocking = {
blockeeId: mongo.ObjectID; blockeeId: mongo.ObjectID;
blockerId: mongo.ObjectID; blockerId: mongo.ObjectID;
}; };
export const packMany = async (
blockings: (string | mongo.ObjectID | IBlocking)[],
me?: string | mongo.ObjectID | IUser
) => {
return (await Promise.all(blockings.map(x => pack(x, me)))).filter(x => x != null);
};
export const pack = (
blocking: any,
me?: any
) => new Promise<any>(async (resolve, reject) => {
let _blocking: any;
// Populate the blocking if 'blocking' is ID
if (isObjectId(blocking)) {
_blocking = await Blocking.findOne({
_id: blocking
});
} else if (typeof blocking === 'string') {
_blocking = await Blocking.findOne({
_id: new mongo.ObjectID(blocking)
});
} else {
_blocking = deepcopy(blocking);
}
// Rename _id to id
_blocking.id = _blocking._id;
delete _blocking._id;
// Populate blockee
_blocking.blockee = await packUser(_blocking.blockeeId, me);
resolve(_blocking);
});

View File

@ -1,7 +1,12 @@
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import db from '../db/mongodb'; import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
const deepcopy = require('deepcopy');
import { pack as packUser } from './user';
const Mute = db.get<IMute>('mute'); const Mute = db.get<IMute>('mute');
Mute.createIndex('muterId');
Mute.createIndex('muteeId');
Mute.createIndex(['muterId', 'muteeId'], { unique: true }); Mute.createIndex(['muterId', 'muteeId'], { unique: true });
export default Mute; export default Mute;
@ -11,3 +16,39 @@ export interface IMute {
muterId: mongo.ObjectID; muterId: mongo.ObjectID;
muteeId: mongo.ObjectID; muteeId: mongo.ObjectID;
} }
export const packMany = async (
mutes: (string | mongo.ObjectID | IMute)[],
me?: string | mongo.ObjectID | IUser
) => {
return (await Promise.all(mutes.map(x => pack(x, me)))).filter(x => x != null);
};
export const pack = (
mute: any,
me?: any
) => new Promise<any>(async (resolve, reject) => {
let _mute: any;
// Populate the mute if 'mute' is ID
if (isObjectId(mute)) {
_mute = await Mute.findOne({
_id: mute
});
} else if (typeof mute === 'string') {
_mute = await Mute.findOne({
_id: new mongo.ObjectID(mute)
});
} else {
_mute = deepcopy(mute);
}
// Rename _id to id
_mute.id = _mute._id;
delete _mute._id;
// Populate mutee
_mute.mutee = await packUser(_mute.muteeId, me);
resolve(_mute);
});

View File

@ -0,0 +1,64 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Blocking, { packMany } from '../../../../models/blocking';
import { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params';
export const meta = {
desc: {
'ja-JP': 'ブロックしているユーザー一覧を取得します。',
'en-US': 'Get blocking users.'
},
requireCredential: true,
kind: 'following-read',
params: {
limit: $.num.optional.range(1, 100).note({
default: 30
}),
sinceId: $.type(ID).optional.note({
}),
untilId: $.type(ID).optional.note({
}),
}
};
export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
const [ps, psErr] = getParams(meta, params);
if (psErr) return rej(psErr);
// Check if both of sinceId and untilId is specified
if (ps.sinceId && ps.untilId) {
return rej('cannot set sinceId and untilId');
}
const query = {
blockerId: me._id
} as any;
const sort = {
_id: -1
};
if (ps.sinceId) {
sort._id = 1;
query._id = {
$gt: ps.sinceId
};
} else if (ps.untilId) {
query._id = {
$lt: ps.untilId
};
}
const blockings = await Blocking
.find(query, {
limit: ps.limit,
sort: sort
});
res(await packMany(blockings, me));
});

View File

@ -1,7 +1,7 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Mute from '../../../../models/mute'; import Mute, { packMany } from '../../../../models/mute';
import { pack, ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
import { getFriendIds } from '../../common/get-friends'; import getParams from '../../get-params';
export const meta = { export const meta = {
desc: { desc: {
@ -11,64 +11,54 @@ export const meta = {
requireCredential: true, requireCredential: true,
kind: 'account/read' kind: 'account/read',
params: {
limit: $.num.optional.range(1, 100).note({
default: 30
}),
sinceId: $.type(ID).optional.note({
}),
untilId: $.type(ID).optional.note({
}),
}
}; };
export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
// Get 'iknow' parameter const [ps, psErr] = getParams(meta, params);
const [iknow = false, iknowErr] = $.bool.optional.get(params.iknow); if (psErr) return rej(psErr);
if (iknowErr) return rej('invalid iknow param');
// Get 'limit' parameter // Check if both of sinceId and untilId is specified
const [limit = 30, limitErr] = $.num.optional.range(1, 100).get(params.limit); if (ps.sinceId && ps.untilId) {
if (limitErr) return rej('invalid limit param'); return rej('cannot set sinceId and untilId');
}
// Get 'cursor' parameter
const [cursor = null, cursorErr] = $.type(ID).optional.get(params.cursor);
if (cursorErr) return rej('invalid cursor param');
// Construct query
const query = { const query = {
muterId: me._id, muterId: me._id
deletedAt: { $exists: false }
} as any; } as any;
if (iknow) { const sort = {
// Get my friends _id: -1
const myFriends = await getFriendIds(me._id);
query.muteeId = {
$in: myFriends
}; };
}
// カーソルが指定されている場合 if (ps.sinceId) {
if (cursor) { sort._id = 1;
query._id = { query._id = {
$lt: cursor $gt: ps.sinceId
};
} else if (ps.untilId) {
query._id = {
$lt: ps.untilId
}; };
} }
// Get mutes
const mutes = await Mute const mutes = await Mute
.find(query, { .find(query, {
limit: limit + 1, limit: ps.limit,
sort: { _id: -1 } sort: sort
}); });
// 「次のページ」があるかどうか res(await packMany(mutes, me));
const inStock = mutes.length === limit + 1;
if (inStock) {
mutes.pop();
}
// Serialize
const users = await Promise.all(mutes.map(async m =>
await pack(m.muteeId, me, { detail: true })));
// Response
res({
users: users,
next: inStock ? mutes[mutes.length - 1]._id : null,
});
}); });