forked from FoundKeyGang/FoundKey
commit
e06dd199a7
24 changed files with 641 additions and 21 deletions
|
@ -346,6 +346,9 @@ desktop:
|
|||
failed: "Failed to setup. please ensure that the token is correct."
|
||||
info: "From the next sign in, enter the token that is displayed on the device in addition to the password."
|
||||
|
||||
mk-mute-setting:
|
||||
no-users: "No muted users"
|
||||
|
||||
mk-post-form:
|
||||
post-placeholder: "What's happening?"
|
||||
reply-placeholder: "Reply to this post..."
|
||||
|
@ -379,6 +382,7 @@ desktop:
|
|||
|
||||
mk-settings:
|
||||
profile: "Profile"
|
||||
mute: "Mute"
|
||||
drive: "Drive"
|
||||
security: "Security"
|
||||
password: "Password"
|
||||
|
@ -473,6 +477,11 @@ desktop:
|
|||
mk-user:
|
||||
last-used-at: "Last used at"
|
||||
|
||||
follows-you: "Follows you"
|
||||
mute: "Mute"
|
||||
muted: "Muting"
|
||||
unmute: "Unmute"
|
||||
|
||||
photos:
|
||||
title: "Photos"
|
||||
loading: "Loading"
|
||||
|
|
|
@ -346,6 +346,9 @@ desktop:
|
|||
failed: "設定に失敗しました。トークンに誤りがないかご確認ください。"
|
||||
info: "次回サインインからは、同様にパスワードに加えてデバイスに表示されているトークンを入力します。"
|
||||
|
||||
mk-mute-setting:
|
||||
no-users: "ミュートしているユーザーはいません"
|
||||
|
||||
mk-post-form:
|
||||
post-placeholder: "いまどうしてる?"
|
||||
reply-placeholder: "この投稿への返信..."
|
||||
|
@ -379,6 +382,7 @@ desktop:
|
|||
|
||||
mk-settings:
|
||||
profile: "プロフィール"
|
||||
mute: "ミュート"
|
||||
drive: "ドライブ"
|
||||
security: "セキュリティ"
|
||||
password: "パスワード"
|
||||
|
@ -473,6 +477,11 @@ desktop:
|
|||
mk-user:
|
||||
last-used-at: "最終アクセス"
|
||||
|
||||
follows-you: "フォローされています"
|
||||
mute: "ミュートする"
|
||||
muted: "ミュートしています"
|
||||
unmute: "ミュート解除"
|
||||
|
||||
photos:
|
||||
title: "フォト"
|
||||
loading: "読み込み中"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as mongo from 'mongodb';
|
||||
import Notification from '../models/notification';
|
||||
import Mute from '../models/mute';
|
||||
import event from '../event';
|
||||
import serialize from '../serializers/notification';
|
||||
|
||||
|
@ -32,6 +33,17 @@ export default (
|
|||
setTimeout(async () => {
|
||||
const fresh = await Notification.findOne({ _id: notification._id }, { is_read: true });
|
||||
if (!fresh.is_read) {
|
||||
//#region ただしミュートしているユーザーからの通知なら無視
|
||||
const mute = await Mute.find({
|
||||
muter_id: notifiee,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
const mutedUserIds = mute.map(m => m.mutee_id.toString());
|
||||
if (mutedUserIds.indexOf(notifier.toString()) != -1) {
|
||||
return;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
event(notifiee, 'unread_notification', await serialize(notification));
|
||||
}
|
||||
}, 3000);
|
||||
|
|
|
@ -222,6 +222,23 @@ const endpoints: Endpoint[] = [
|
|||
withCredential: true,
|
||||
kind: 'notification-read'
|
||||
},
|
||||
|
||||
{
|
||||
name: 'mute/create',
|
||||
withCredential: true,
|
||||
kind: 'account/write'
|
||||
},
|
||||
{
|
||||
name: 'mute/delete',
|
||||
withCredential: true,
|
||||
kind: 'account/write'
|
||||
},
|
||||
{
|
||||
name: 'mute/list',
|
||||
withCredential: true,
|
||||
kind: 'account/read'
|
||||
},
|
||||
|
||||
{
|
||||
name: 'notifications/get_unread_count',
|
||||
withCredential: true,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
import $ from 'cafy';
|
||||
import Notification from '../../models/notification';
|
||||
import Mute from '../../models/mute';
|
||||
import serialize from '../../serializers/notification';
|
||||
import getFriends from '../../common/get-friends';
|
||||
import read from '../../common/read-notification';
|
||||
|
@ -45,8 +46,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
return rej('cannot set since_id and until_id');
|
||||
}
|
||||
|
||||
const mute = await Mute.find({
|
||||
muter_id: user._id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
const query = {
|
||||
notifiee_id: user._id
|
||||
notifiee_id: user._id,
|
||||
$and: [{
|
||||
notifier_id: {
|
||||
$nin: mute.map(m => m.mutee_id)
|
||||
}
|
||||
}]
|
||||
} as any;
|
||||
|
||||
const sort = {
|
||||
|
@ -54,12 +65,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
};
|
||||
|
||||
if (following) {
|
||||
// ID list of the user $self and other users who the user follows
|
||||
// ID list of the user itself and other users who the user follows
|
||||
const followingIds = await getFriends(user._id);
|
||||
|
||||
query.notifier_id = {
|
||||
$in: followingIds
|
||||
};
|
||||
query.$and.push({
|
||||
notifier_id: {
|
||||
$in: followingIds
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (type) {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
import $ from 'cafy';
|
||||
import History from '../../models/messaging-history';
|
||||
import Mute from '../../models/mute';
|
||||
import serialize from '../../serializers/messaging-message';
|
||||
|
||||
/**
|
||||
|
@ -17,10 +18,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
||||
if (limitErr) return rej('invalid limit param');
|
||||
|
||||
const mute = await Mute.find({
|
||||
muter_id: user._id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
// Get history
|
||||
const history = await History
|
||||
.find({
|
||||
user_id: user._id
|
||||
user_id: user._id,
|
||||
partner: {
|
||||
$nin: mute.map(m => m.mutee_id)
|
||||
}
|
||||
}, {
|
||||
limit: limit,
|
||||
sort: {
|
||||
|
|
|
@ -6,6 +6,7 @@ import Message from '../../../models/messaging-message';
|
|||
import { isValidText } from '../../../models/messaging-message';
|
||||
import History from '../../../models/messaging-history';
|
||||
import User from '../../../models/user';
|
||||
import Mute from '../../../models/mute';
|
||||
import DriveFile from '../../../models/drive-file';
|
||||
import serialize from '../../../serializers/messaging-message';
|
||||
import publishUserStream from '../../../event';
|
||||
|
@ -97,6 +98,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
setTimeout(async () => {
|
||||
const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true });
|
||||
if (!freshMessage.is_read) {
|
||||
//#region ただしミュートされているなら発行しない
|
||||
const mute = await Mute.find({
|
||||
muter_id: recipient._id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
const mutedUserIds = mute.map(m => m.mutee_id.toString());
|
||||
if (mutedUserIds.indexOf(user._id.toString()) != -1) {
|
||||
return;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj);
|
||||
pushSw(message.recipient_id, 'unread_messaging_message', messageObj);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* Module dependencies
|
||||
*/
|
||||
import Message from '../../models/messaging-message';
|
||||
import Mute from '../../models/mute';
|
||||
|
||||
/**
|
||||
* Get count of unread messages
|
||||
|
@ -11,8 +12,17 @@ import Message from '../../models/messaging-message';
|
|||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
const mute = await Mute.find({
|
||||
muter_id: user._id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
const mutedUserIds = mute.map(m => m.mutee_id);
|
||||
|
||||
const count = await Message
|
||||
.count({
|
||||
user_id: {
|
||||
$nin: mutedUserIds
|
||||
},
|
||||
recipient_id: user._id,
|
||||
is_read: false
|
||||
});
|
||||
|
|
61
src/api/endpoints/mute/create.ts
Normal file
61
src/api/endpoints/mute/create.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import User from '../../models/user';
|
||||
import Mute from '../../models/mute';
|
||||
|
||||
/**
|
||||
* Mute a user
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
const muter = user;
|
||||
|
||||
// Get 'user_id' parameter
|
||||
const [userId, userIdErr] = $(params.user_id).id().$;
|
||||
if (userIdErr) return rej('invalid user_id param');
|
||||
|
||||
// 自分自身
|
||||
if (user._id.equals(userId)) {
|
||||
return rej('mutee is yourself');
|
||||
}
|
||||
|
||||
// Get mutee
|
||||
const mutee = await User.findOne({
|
||||
_id: userId
|
||||
}, {
|
||||
fields: {
|
||||
data: false,
|
||||
profile: false
|
||||
}
|
||||
});
|
||||
|
||||
if (mutee === null) {
|
||||
return rej('user not found');
|
||||
}
|
||||
|
||||
// Check if already muting
|
||||
const exist = await Mute.findOne({
|
||||
muter_id: muter._id,
|
||||
mutee_id: mutee._id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
if (exist !== null) {
|
||||
return rej('already muting');
|
||||
}
|
||||
|
||||
// Create mute
|
||||
await Mute.insert({
|
||||
created_at: new Date(),
|
||||
muter_id: muter._id,
|
||||
mutee_id: mutee._id,
|
||||
});
|
||||
|
||||
// Send response
|
||||
res();
|
||||
});
|
63
src/api/endpoints/mute/delete.ts
Normal file
63
src/api/endpoints/mute/delete.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import User from '../../models/user';
|
||||
import Mute from '../../models/mute';
|
||||
|
||||
/**
|
||||
* Unmute a user
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
const muter = user;
|
||||
|
||||
// Get 'user_id' parameter
|
||||
const [userId, userIdErr] = $(params.user_id).id().$;
|
||||
if (userIdErr) return rej('invalid user_id param');
|
||||
|
||||
// Check if the mutee is yourself
|
||||
if (user._id.equals(userId)) {
|
||||
return rej('mutee is yourself');
|
||||
}
|
||||
|
||||
// Get mutee
|
||||
const mutee = await User.findOne({
|
||||
_id: userId
|
||||
}, {
|
||||
fields: {
|
||||
data: false,
|
||||
profile: false
|
||||
}
|
||||
});
|
||||
|
||||
if (mutee === null) {
|
||||
return rej('user not found');
|
||||
}
|
||||
|
||||
// Check not muting
|
||||
const exist = await Mute.findOne({
|
||||
muter_id: muter._id,
|
||||
mutee_id: mutee._id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
if (exist === null) {
|
||||
return rej('already not muting');
|
||||
}
|
||||
|
||||
// Delete mute
|
||||
await Mute.update({
|
||||
_id: exist._id
|
||||
}, {
|
||||
$set: {
|
||||
deleted_at: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
// Send response
|
||||
res();
|
||||
});
|
73
src/api/endpoints/mute/list.ts
Normal file
73
src/api/endpoints/mute/list.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import Mute from '../../models/mute';
|
||||
import serialize from '../../serializers/user';
|
||||
import getFriends from '../../common/get-friends';
|
||||
|
||||
/**
|
||||
* Get muted users of a user
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} me
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||
// Get 'iknow' parameter
|
||||
const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$;
|
||||
if (iknowErr) return rej('invalid iknow param');
|
||||
|
||||
// Get 'limit' parameter
|
||||
const [limit = 30, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
||||
if (limitErr) return rej('invalid limit param');
|
||||
|
||||
// Get 'cursor' parameter
|
||||
const [cursor = null, cursorErr] = $(params.cursor).optional.id().$;
|
||||
if (cursorErr) return rej('invalid cursor param');
|
||||
|
||||
// Construct query
|
||||
const query = {
|
||||
muter_id: me._id,
|
||||
deleted_at: { $exists: false }
|
||||
} as any;
|
||||
|
||||
if (iknow) {
|
||||
// Get my friends
|
||||
const myFriends = await getFriends(me._id);
|
||||
|
||||
query.mutee_id = {
|
||||
$in: myFriends
|
||||
};
|
||||
}
|
||||
|
||||
// カーソルが指定されている場合
|
||||
if (cursor) {
|
||||
query._id = {
|
||||
$lt: cursor
|
||||
};
|
||||
}
|
||||
|
||||
// Get mutes
|
||||
const mutes = await Mute
|
||||
.find(query, {
|
||||
limit: limit + 1,
|
||||
sort: { _id: -1 }
|
||||
});
|
||||
|
||||
// 「次のページ」があるかどうか
|
||||
const inStock = mutes.length === limit + 1;
|
||||
if (inStock) {
|
||||
mutes.pop();
|
||||
}
|
||||
|
||||
// Serialize
|
||||
const users = await Promise.all(mutes.map(async m =>
|
||||
await serialize(m.mutee_id, me, { detail: true })));
|
||||
|
||||
// Response
|
||||
res({
|
||||
users: users,
|
||||
next: inStock ? mutes[mutes.length - 1]._id : null,
|
||||
});
|
||||
});
|
|
@ -2,6 +2,7 @@
|
|||
* Module dependencies
|
||||
*/
|
||||
import Notification from '../../models/notification';
|
||||
import Mute from '../../models/mute';
|
||||
|
||||
/**
|
||||
* Get count of unread notifications
|
||||
|
@ -11,9 +12,18 @@ import Notification from '../../models/notification';
|
|||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
const mute = await Mute.find({
|
||||
muter_id: user._id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
const mutedUserIds = mute.map(m => m.mutee_id);
|
||||
|
||||
const count = await Notification
|
||||
.count({
|
||||
notifiee_id: user._id,
|
||||
notifier_id: {
|
||||
$nin: mutedUserIds
|
||||
},
|
||||
is_read: false
|
||||
});
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { default as Post, IPost, isValidText } from '../../models/post';
|
|||
import { default as User, IUser } from '../../models/user';
|
||||
import { default as Channel, IChannel } from '../../models/channel';
|
||||
import Following from '../../models/following';
|
||||
import Mute from '../../models/mute';
|
||||
import DriveFile from '../../models/drive-file';
|
||||
import Watching from '../../models/post-watching';
|
||||
import ChannelWatching from '../../models/channel-watching';
|
||||
|
@ -215,7 +216,11 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
poll: poll,
|
||||
text: text,
|
||||
user_id: user._id,
|
||||
app_id: app ? app._id : null
|
||||
app_id: app ? app._id : null,
|
||||
|
||||
// 以下非正規化データ
|
||||
_reply: reply ? { user_id: reply.user_id } : undefined,
|
||||
_repost: repost ? { user_id: repost.user_id } : undefined,
|
||||
});
|
||||
|
||||
// Serialize
|
||||
|
@ -236,7 +241,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
|
||||
const mentions = [];
|
||||
|
||||
function addMention(mentionee, reason) {
|
||||
async function addMention(mentionee, reason) {
|
||||
// Reject if already added
|
||||
if (mentions.some(x => x.equals(mentionee))) return;
|
||||
|
||||
|
@ -245,8 +250,15 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
|
||||
// Publish event
|
||||
if (!user._id.equals(mentionee)) {
|
||||
event(mentionee, reason, postObj);
|
||||
pushSw(mentionee, reason, postObj);
|
||||
const mentioneeMutes = await Mute.find({
|
||||
muter_id: mentionee,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
const mentioneesMutedUserIds = mentioneeMutes.map(m => m.mutee_id.toString());
|
||||
if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
|
||||
event(mentionee, reason, postObj);
|
||||
pushSw(mentionee, reason, postObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import $ from 'cafy';
|
|||
const escapeRegexp = require('escape-regexp');
|
||||
import Post from '../../models/post';
|
||||
import User from '../../models/user';
|
||||
import Mute from '../../models/mute';
|
||||
import getFriends from '../../common/get-friends';
|
||||
import serialize from '../../serializers/post';
|
||||
import config from '../../../conf';
|
||||
|
@ -34,6 +35,10 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
|||
const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
|
||||
if (followingErr) return rej('invalid following param');
|
||||
|
||||
// Get 'mute' parameter
|
||||
const [mute = 'mute_all', muteErr] = $(params.mute).optional.string().$;
|
||||
if (muteErr) return rej('invalid mute param');
|
||||
|
||||
// Get 'reply' parameter
|
||||
const [reply = null, replyErr] = $(params.reply).optional.nullable.boolean().$;
|
||||
if (replyErr) return rej('invalid reply param');
|
||||
|
@ -80,11 +85,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
|||
// If Elasticsearch is available, search by it
|
||||
// If not, search by MongoDB
|
||||
(config.elasticsearch.enable ? byElasticsearch : byNative)
|
||||
(res, rej, me, text, user, following, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
|
||||
(res, rej, me, text, user, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
|
||||
});
|
||||
|
||||
// Search by MongoDB
|
||||
async function byNative(res, rej, me, text, userId, following, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
|
||||
async function byNative(res, rej, me, text, userId, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
|
||||
let q: any = {
|
||||
$and: []
|
||||
};
|
||||
|
@ -116,6 +121,84 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
|
|||
});
|
||||
}
|
||||
|
||||
if (me != null) {
|
||||
const mutes = await Mute.find({
|
||||
muter_id: me._id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
const mutedUserIds = mutes.map(m => m.mutee_id);
|
||||
|
||||
switch (mute) {
|
||||
case 'mute_all':
|
||||
push({
|
||||
user_id: {
|
||||
$nin: mutedUserIds
|
||||
},
|
||||
'_reply.user_id': {
|
||||
$nin: mutedUserIds
|
||||
},
|
||||
'_repost.user_id': {
|
||||
$nin: mutedUserIds
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'mute_related':
|
||||
push({
|
||||
'_reply.user_id': {
|
||||
$nin: mutedUserIds
|
||||
},
|
||||
'_repost.user_id': {
|
||||
$nin: mutedUserIds
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'mute_direct':
|
||||
push({
|
||||
user_id: {
|
||||
$nin: mutedUserIds
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'direct_only':
|
||||
push({
|
||||
user_id: {
|
||||
$in: mutedUserIds
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'related_only':
|
||||
push({
|
||||
$or: [{
|
||||
'_reply.user_id': {
|
||||
$in: mutedUserIds
|
||||
}
|
||||
}, {
|
||||
'_repost.user_id': {
|
||||
$in: mutedUserIds
|
||||
}
|
||||
}]
|
||||
});
|
||||
break;
|
||||
case 'all_only':
|
||||
push({
|
||||
$or: [{
|
||||
user_id: {
|
||||
$in: mutedUserIds
|
||||
}
|
||||
}, {
|
||||
'_reply.user_id': {
|
||||
$in: mutedUserIds
|
||||
}
|
||||
}, {
|
||||
'_repost.user_id': {
|
||||
$in: mutedUserIds
|
||||
}
|
||||
}]
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (reply != null) {
|
||||
if (reply) {
|
||||
push({
|
||||
|
@ -236,7 +319,7 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
|
|||
}
|
||||
|
||||
// Search by Elasticsearch
|
||||
async function byElasticsearch(res, rej, me, text, userId, following, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
|
||||
async function byElasticsearch(res, rej, me, text, userId, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
|
||||
const es = require('../../db/elasticsearch');
|
||||
|
||||
es.search({
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import $ from 'cafy';
|
||||
import rap from '@prezzemolo/rap';
|
||||
import Post from '../../models/post';
|
||||
import Mute from '../../models/mute';
|
||||
import ChannelWatching from '../../models/channel-watching';
|
||||
import getFriends from '../../common/get-friends';
|
||||
import serialize from '../../serializers/post';
|
||||
|
@ -42,15 +43,23 @@ module.exports = async (params, user, app) => {
|
|||
throw 'only one of since_id, until_id, since_date, until_date can be specified';
|
||||
}
|
||||
|
||||
const { followingIds, watchingChannelIds } = await rap({
|
||||
const { followingIds, watchingChannelIds, mutedUserIds } = await rap({
|
||||
// ID list of the user itself and other users who the user follows
|
||||
followingIds: getFriends(user._id),
|
||||
|
||||
// Watchしているチャンネルを取得
|
||||
watchingChannelIds: ChannelWatching.find({
|
||||
user_id: user._id,
|
||||
// 削除されたドキュメントは除く
|
||||
deleted_at: { $exists: false }
|
||||
}).then(watches => watches.map(w => w.channel_id))
|
||||
}).then(watches => watches.map(w => w.channel_id)),
|
||||
|
||||
// ミュートしているユーザーを取得
|
||||
mutedUserIds: Mute.find({
|
||||
muter_id: user._id,
|
||||
// 削除されたドキュメントは除く
|
||||
deleted_at: { $exists: false }
|
||||
}).then(ms => ms.map(m => m.mutee_id))
|
||||
});
|
||||
|
||||
//#region Construct query
|
||||
|
@ -77,7 +86,17 @@ module.exports = async (params, user, app) => {
|
|||
channel_id: {
|
||||
$in: watchingChannelIds
|
||||
}
|
||||
}]
|
||||
}],
|
||||
// mute
|
||||
user_id: {
|
||||
$nin: mutedUserIds
|
||||
},
|
||||
'_reply.user_id': {
|
||||
$nin: mutedUserIds
|
||||
},
|
||||
'_repost.user_id': {
|
||||
$nin: mutedUserIds
|
||||
},
|
||||
} as any;
|
||||
|
||||
if (sinceId) {
|
||||
|
|
3
src/api/models/mute.ts
Normal file
3
src/api/models/mute.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import db from '../../db/mongodb';
|
||||
|
||||
export default db.get('mute') as any; // fuck type definition
|
|
@ -6,6 +6,7 @@ import deepcopy = require('deepcopy');
|
|||
import { default as User, IUser } from '../models/user';
|
||||
import serializePost from './post';
|
||||
import Following from '../models/following';
|
||||
import Mute from '../models/mute';
|
||||
import getFriends from '../common/get-friends';
|
||||
import config from '../../conf';
|
||||
import rap from '@prezzemolo/rap';
|
||||
|
@ -113,7 +114,7 @@ export default (
|
|||
}
|
||||
|
||||
if (meId && !meId.equals(_user.id)) {
|
||||
// If the user is following
|
||||
// Whether the user is following
|
||||
_user.is_following = (async () => {
|
||||
const follow = await Following.findOne({
|
||||
follower_id: meId,
|
||||
|
@ -123,7 +124,7 @@ export default (
|
|||
return follow !== null;
|
||||
})();
|
||||
|
||||
// If the user is followed
|
||||
// Whether the user is followed
|
||||
_user.is_followed = (async () => {
|
||||
const follow2 = await Following.findOne({
|
||||
follower_id: _user.id,
|
||||
|
@ -132,6 +133,16 @@ export default (
|
|||
});
|
||||
return follow2 !== null;
|
||||
})();
|
||||
|
||||
// Whether the user is muted
|
||||
_user.is_muted = (async () => {
|
||||
const mute = await Mute.findOne({
|
||||
muter_id: meId,
|
||||
mutee_id: _user.id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
return mute !== null;
|
||||
})();
|
||||
}
|
||||
|
||||
if (opts.detail) {
|
||||
|
|
|
@ -3,19 +3,48 @@ import * as redis from 'redis';
|
|||
import * as debug from 'debug';
|
||||
|
||||
import User from '../models/user';
|
||||
import Mute from '../models/mute';
|
||||
import serializePost from '../serializers/post';
|
||||
import readNotification from '../common/read-notification';
|
||||
|
||||
const log = debug('misskey');
|
||||
|
||||
export default function homeStream(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
|
||||
export default async function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any) {
|
||||
// Subscribe Home stream channel
|
||||
subscriber.subscribe(`misskey:user-stream:${user._id}`);
|
||||
|
||||
const mute = await Mute.find({
|
||||
muter_id: user._id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
const mutedUserIds = mute.map(m => m.mutee_id.toString());
|
||||
|
||||
subscriber.on('message', async (channel, data) => {
|
||||
switch (channel.split(':')[1]) {
|
||||
case 'user-stream':
|
||||
connection.send(data);
|
||||
try {
|
||||
const x = JSON.parse(data);
|
||||
|
||||
if (x.type == 'post') {
|
||||
if (mutedUserIds.indexOf(x.body.user_id) != -1) {
|
||||
return;
|
||||
}
|
||||
if (x.body.reply != null && mutedUserIds.indexOf(x.body.reply.user_id) != -1) {
|
||||
return;
|
||||
}
|
||||
if (x.body.repost != null && mutedUserIds.indexOf(x.body.repost.user_id) != -1) {
|
||||
return;
|
||||
}
|
||||
} else if (x.type == 'notification') {
|
||||
if (mutedUserIds.indexOf(x.body.user_id) != -1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
connection.send(data);
|
||||
} catch (e) {
|
||||
connection.send(data);
|
||||
}
|
||||
break;
|
||||
case 'post-stream':
|
||||
const postId = channel.split(':')[2];
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<p class={ active: page == 'web' } onmousedown={ setPage.bind(null, 'web') }>%fa:desktop .fw%Web</p>
|
||||
<p class={ active: page == 'notification' } onmousedown={ setPage.bind(null, 'notification') }>%fa:R bell .fw%通知</p>
|
||||
<p class={ active: page == 'drive' } onmousedown={ setPage.bind(null, 'drive') }>%fa:cloud .fw%%i18n:desktop.tags.mk-settings.drive%</p>
|
||||
<p class={ active: page == 'mute' } onmousedown={ setPage.bind(null, 'mute') }>%fa:ban .fw%%i18n:desktop.tags.mk-settings.mute%</p>
|
||||
<p class={ active: page == 'apps' } onmousedown={ setPage.bind(null, 'apps') }>%fa:puzzle-piece .fw%アプリ</p>
|
||||
<p class={ active: page == 'twitter' } onmousedown={ setPage.bind(null, 'twitter') }>%fa:B twitter .fw%Twitter</p>
|
||||
<p class={ active: page == 'security' } onmousedown={ setPage.bind(null, 'security') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p>
|
||||
|
@ -26,6 +27,11 @@
|
|||
<mk-drive-setting/>
|
||||
</section>
|
||||
|
||||
<section class="mute" show={ page == 'mute' }>
|
||||
<h1>%i18n:desktop.tags.mk-settings.mute%</h1>
|
||||
<mk-mute-setting/>
|
||||
</section>
|
||||
|
||||
<section class="apps" show={ page == 'apps' }>
|
||||
<h1>アプリケーション</h1>
|
||||
<mk-authorized-apps/>
|
||||
|
@ -386,3 +392,35 @@
|
|||
});
|
||||
</script>
|
||||
</mk-drive-setting>
|
||||
|
||||
<mk-mute-setting>
|
||||
<div class="none ui info" if={ !fetching && users.length == 0 }>
|
||||
<p>%fa:info-circle%%i18n:desktop.tags.mk-mute-setting.no-users%</p>
|
||||
</div>
|
||||
<div class="users" if={ users.length != 0 }>
|
||||
<div each={ user in users }>
|
||||
<p><b>{ user.name }</b> @{ user.username }</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('api');
|
||||
|
||||
this.apps = [];
|
||||
this.fetching = true;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.api('mute/list').then(x => {
|
||||
this.update({
|
||||
fetching: false,
|
||||
users: x.users
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</mk-mute-setting>
|
||||
|
|
|
@ -226,7 +226,9 @@
|
|||
<mk-user-profile>
|
||||
<div class="friend-form" if={ SIGNIN && I.id != user.id }>
|
||||
<mk-big-follow-button user={ user }/>
|
||||
<p class="followed" if={ user.is_followed }>フォローされています</p>
|
||||
<p class="followed" if={ user.is_followed }>%i18n:desktop.tags.mk-user.follows-you%</p>
|
||||
<p if={ user.is_muted }>%i18n:desktop.tags.mk-user.muted% <a onclick={ unmute }>%i18n:desktop.tags.mk-user.unmute%</a></p>
|
||||
<p if={ !user.is_muted }><a onclick={ mute }>%i18n:desktop.tags.mk-user.mute%</a></p>
|
||||
</div>
|
||||
<div class="description" if={ user.description }>{ user.description }</div>
|
||||
<div class="birthday" if={ user.profile.birthday }>
|
||||
|
@ -311,6 +313,7 @@
|
|||
this.age = require('s-age');
|
||||
|
||||
this.mixin('i');
|
||||
this.mixin('api');
|
||||
|
||||
this.user = this.opts.user;
|
||||
|
||||
|
@ -325,6 +328,28 @@
|
|||
user: this.user
|
||||
});
|
||||
};
|
||||
|
||||
this.mute = () => {
|
||||
this.api('mute/create', {
|
||||
user_id: this.user.id
|
||||
}).then(() => {
|
||||
this.user.is_muted = true;
|
||||
this.update();
|
||||
}, e => {
|
||||
alert('error');
|
||||
});
|
||||
};
|
||||
|
||||
this.unmute = () => {
|
||||
this.api('mute/delete', {
|
||||
user_id: this.user.id
|
||||
}).then(() => {
|
||||
this.user.is_muted = false;
|
||||
this.update();
|
||||
}, e => {
|
||||
alert('error');
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-user-profile>
|
||||
|
||||
|
|
|
@ -75,6 +75,12 @@ props:
|
|||
optional: true
|
||||
desc:
|
||||
ja: "自分がこのユーザーにフォローされているか"
|
||||
- name: "is_muted"
|
||||
type: "boolean"
|
||||
optional: true
|
||||
desc:
|
||||
ja: "自分がこのユーザーをミュートしているか"
|
||||
en: "Whether you muted this user"
|
||||
- name: "last_used_at"
|
||||
type: "date"
|
||||
optional: false
|
||||
|
|
13
src/web/docs/mute.ja.pug
Normal file
13
src/web/docs/mute.ja.pug
Normal file
|
@ -0,0 +1,13 @@
|
|||
h1 ミュート
|
||||
|
||||
p ユーザーページから、そのユーザーをミュートすることができます。
|
||||
|
||||
p ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります:
|
||||
ul
|
||||
li タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRepost)
|
||||
li そのユーザーからの通知
|
||||
li メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴
|
||||
|
||||
p ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません。
|
||||
|
||||
p 設定>ミュート から、自分がミュートしているユーザー一覧を確認することができます。
|
|
@ -29,6 +29,22 @@ section
|
|||
| false ... フォローしていないユーザーに限定。
|
||||
br
|
||||
| null ... 特に限定しない(デフォルト)
|
||||
tr
|
||||
td mute
|
||||
td
|
||||
| mute_all ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostを除外する(デフォルト)
|
||||
br
|
||||
| mute_related ... ミュートしているユーザーの投稿に対する返信やRepostだけ除外する
|
||||
br
|
||||
| mute_direct ... ミュートしているユーザーの投稿だけ除外する
|
||||
br
|
||||
| disabled ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostも含める
|
||||
br
|
||||
| direct_only ... ミュートしているユーザーの投稿だけに限定
|
||||
br
|
||||
| related_only ... ミュートしているユーザーの投稿に対する返信やRepostだけに限定
|
||||
br
|
||||
| all_only ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostに限定
|
||||
tr
|
||||
td reply
|
||||
td
|
||||
|
|
67
tools/migration/node.2017-12-22.hiseikika.js
Normal file
67
tools/migration/node.2017-12-22.hiseikika.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
// for Node.js interpret
|
||||
|
||||
const { default: Post } = require('../../built/api/models/post')
|
||||
const { default: zip } = require('@prezzemolo/zip')
|
||||
|
||||
const migrate = async (post) => {
|
||||
const x = {};
|
||||
if (post.reply_id != null) {
|
||||
const reply = await Post.findOne({
|
||||
_id: post.reply_id
|
||||
});
|
||||
x['_reply.user_id'] = reply.user_id;
|
||||
}
|
||||
if (post.repost_id != null) {
|
||||
const repost = await Post.findOne({
|
||||
_id: post.repost_id
|
||||
});
|
||||
x['_repost.user_id'] = repost.user_id;
|
||||
}
|
||||
if (post.reply_id != null || post.repost_id != null) {
|
||||
const result = await Post.update(post._id, {
|
||||
$set: x,
|
||||
});
|
||||
return result.ok === 1;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const query = {
|
||||
$or: [{
|
||||
reply_id: {
|
||||
$exists: true,
|
||||
$ne: null
|
||||
}
|
||||
}, {
|
||||
repost_id: {
|
||||
$exists: true,
|
||||
$ne: null
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
const count = await Post.count(query);
|
||||
|
||||
const dop = Number.parseInt(process.argv[2]) || 5
|
||||
const idop = ((count - (count % dop)) / dop) + 1
|
||||
|
||||
return zip(
|
||||
1,
|
||||
async (time) => {
|
||||
console.log(`${time} / ${idop}`)
|
||||
const doc = await Post.find(query, {
|
||||
limit: dop, skip: time * dop
|
||||
})
|
||||
return Promise.all(doc.map(migrate))
|
||||
},
|
||||
idop
|
||||
).then(a => {
|
||||
const rv = []
|
||||
a.forEach(e => rv.push(...e))
|
||||
return rv
|
||||
})
|
||||
}
|
||||
|
||||
main().then(console.dir).catch(console.error)
|
Loading…
Reference in a new issue