Implement Channel Watching

This commit is contained in:
syuilo 2017-11-01 19:33:08 +09:00
parent 2b3937d731
commit d6b03c43eb
9 changed files with 244 additions and 27 deletions

View File

@ -490,6 +490,14 @@ const endpoints: Endpoint[] = [
{ {
name: 'channels/posts' name: 'channels/posts'
}, },
{
name: 'channels/watch',
withCredential: true
},
{
name: 'channels/unwatch',
withCredential: true
},
{ {
name: 'channels' name: 'channels'
}, },

View File

@ -3,6 +3,7 @@
*/ */
import $ from 'cafy'; import $ from 'cafy';
import Channel from '../../models/channel'; import Channel from '../../models/channel';
import Watching from '../../models/channel-watching';
import serialize from '../../serializers/channel'; import serialize from '../../serializers/channel';
/** /**
@ -22,9 +23,17 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
created_at: new Date(), created_at: new Date(),
user_id: user._id, user_id: user._id,
title: title, title: title,
index: 0 index: 0,
watching_count: 1
}); });
// Response // Response
res(await serialize(channel)); res(await serialize(channel));
// Create Watching
await Watching.insert({
created_at: new Date(),
user_id: user._id,
channel_id: channel._id
});
}); });

View File

@ -0,0 +1,60 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Channel from '../../models/channel';
import Watching from '../../models/channel-watching';
/**
* Unwatch a channel
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'channel_id' parameter
const [channelId, channelIdErr] = $(params.channel_id).id().$;
if (channelIdErr) return rej('invalid channel_id param');
//#region Fetch channel
const channel = await Channel.findOne({
_id: channelId
});
if (channel === null) {
return rej('channel not found');
}
//#endregion
//#region Check whether not watching
const exist = await Watching.findOne({
user_id: user._id,
channel_id: channel._id,
deleted_at: { $exists: false }
});
if (exist === null) {
return rej('already not watching');
}
//#endregion
// Delete watching
await Watching.update({
_id: exist._id
}, {
$set: {
deleted_at: new Date()
}
});
// Send response
res();
// Decrement watching count
Channel.update(channel._id, {
$inc: {
watching_count: -1
}
});
});

View File

@ -0,0 +1,58 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Channel from '../../models/channel';
import Watching from '../../models/channel-watching';
/**
* Watch a channel
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'channel_id' parameter
const [channelId, channelIdErr] = $(params.channel_id).id().$;
if (channelIdErr) return rej('invalid channel_id param');
//#region Fetch channel
const channel = await Channel.findOne({
_id: channelId
});
if (channel === null) {
return rej('channel not found');
}
//#endregion
//#region Check whether already watching
const exist = await Watching.findOne({
user_id: user._id,
channel_id: channel._id,
deleted_at: { $exists: false }
});
if (exist !== null) {
return rej('already watching');
}
//#endregion
// Create Watching
await Watching.insert({
created_at: new Date(),
user_id: user._id,
channel_id: channel._id
});
// Send response
res();
// Increment watching count
Channel.update(channel._id, {
$inc: {
watching_count: 1
}
});
});

View File

@ -10,6 +10,7 @@ import { default as Channel, IChannel } from '../../models/channel';
import Following from '../../models/following'; import Following from '../../models/following';
import DriveFile from '../../models/drive-file'; import DriveFile from '../../models/drive-file';
import Watching from '../../models/post-watching'; import Watching from '../../models/post-watching';
import ChannelWatching from '../../models/channel-watching';
import serialize from '../../serializers/post'; import serialize from '../../serializers/post';
import notify from '../../common/notify'; import notify from '../../common/notify';
import watch from '../../common/watch-post'; import watch from '../../common/watch-post';
@ -249,26 +250,11 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
} }
} }
// TODO // タイムラインへの投稿
if (!channel) { if (!channel) {
// Publish event to myself's stream // Publish event to myself's stream
event(user._id, 'post', postObj); event(user._id, 'post', postObj);
}
if (channel) {
// Increment channel index(posts count)
Channel.update({ _id: channel._id }, {
$inc: {
index: 1
}
});
// Publish event to channel
publishChannelStream(channel._id, 'post', postObj);
}
// TODO
if (!channel) {
// Fetch all followers // Fetch all followers
const followers = await Following const followers = await Following
.find({ .find({
@ -285,6 +271,31 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
event(following.follower_id, 'post', postObj)); event(following.follower_id, 'post', postObj));
} }
// チャンネルへの投稿
if (channel) {
// Increment channel index(posts count)
Channel.update({ _id: channel._id }, {
$inc: {
index: 1
}
});
// Publish event to channel
publishChannelStream(channel._id, 'post', postObj);
// Get channel watchers
const watches = await ChannelWatching.find({
channel_id: channel._id,
// 削除されたドキュメントは除く
deleted_at: { $exists: false }
});
// チャンネルの視聴者(のタイムライン)に配信
watches.forEach(w => {
event(w.user_id, 'post', postObj);
});
}
// Increment my posts count // Increment my posts count
User.update({ _id: user._id }, { User.update({ _id: user._id }, {
$inc: { $inc: {

View File

@ -3,6 +3,7 @@
*/ */
import $ from 'cafy'; import $ from 'cafy';
import Post from '../../models/post'; import Post from '../../models/post';
import ChannelWatching from '../../models/channel-watching';
import getFriends from '../../common/get-friends'; import getFriends from '../../common/get-friends';
import serialize from '../../serializers/post'; import serialize from '../../serializers/post';
@ -32,18 +33,28 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
return rej('cannot set since_id and max_id'); return rej('cannot set since_id and max_id');
} }
// 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); const followingIds = await getFriends(user._id);
// Construct query // Watchしているチャンネルを取得
const watches = await ChannelWatching.find({
user_id: user._id,
// 削除されたドキュメントは除く
deleted_at: { $exists: false }
});
//#region Construct query
const sort = { const sort = {
_id: -1 _id: -1
}; };
const query = { const query = {
$or: [{
// フォローしている人のタイムラインへの投稿
user_id: { user_id: {
$in: followingIds $in: followingIds
}, },
// TODO // 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
$or: [{ $or: [{
channel_id: { channel_id: {
$exists: false $exists: false
@ -51,7 +62,14 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
}, { }, {
channel_id: null channel_id: null
}] }]
}, {
// Watchしているチャンネルへの投稿
channel_id: {
$in: watches.map(w => w.channel_id)
}
}]
} as any; } as any;
if (sinceId) { if (sinceId) {
sort._id = 1; sort._id = 1;
query._id = { query._id = {
@ -62,6 +80,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
$lt: maxId $lt: maxId
}; };
} }
//#endregion
// Issue query // Issue query
const timeline = await Post const timeline = await Post

View File

@ -0,0 +1,3 @@
import db from '../../db/mongodb';
export default db.get('channel_watching') as any; // fuck type definition

View File

@ -5,6 +5,7 @@ import * as mongo from 'mongodb';
import deepcopy = require('deepcopy'); import deepcopy = require('deepcopy');
import { IUser } from '../models/user'; import { IUser } from '../models/user';
import { default as Channel, IChannel } from '../models/channel'; import { default as Channel, IChannel } from '../models/channel';
import Watching from '../models/channel-watching';
/** /**
* Serialize a channel * Serialize a channel
@ -40,5 +41,26 @@ export default (
// Remove needless properties // Remove needless properties
delete _channel.user_id; delete _channel.user_id;
// Me
const meId: mongo.ObjectID = me
? mongo.ObjectID.prototype.isPrototypeOf(me)
? me as mongo.ObjectID
: typeof me === 'string'
? new mongo.ObjectID(me)
: (me as IUser)._id
: null;
if (me) {
//#region Watchしているかどうか
const watch = await Watching.findOne({
user_id: meId,
channel_id: _channel.id,
deleted_at: { $exists: false }
});
_channel.is_watching = watch !== null;
//#endregion
}
resolve(_channel); resolve(_channel);
}); });

View File

@ -4,6 +4,11 @@
<main if={ !fetching }> <main if={ !fetching }>
<h1>{ channel.title }</h1> <h1>{ channel.title }</h1>
<div if={ SIGNIN }>
<p if={ channel.is_watching }>このチャンネルをウォッチしています <a onclick={ unwatch }>ウォッチ解除</a></p>
<p if={ !channel.is_watching }><a onclick={ watch }>このチャンネルをウォッチする</a></p>
</div>
<div class="share"> <div class="share">
<mk-twitter-button/> <mk-twitter-button/>
<mk-line-button/> <mk-line-button/>
@ -131,6 +136,28 @@
document.title = this.channel.title + ' | Misskey' document.title = this.channel.title + ' | Misskey'
} }
}; };
this.watch = () => {
this.api('channels/watch', {
channel_id: this.id
}).then(() => {
this.channel.is_watching = true;
this.update();
}, e => {
alert('error');
});
};
this.unwatch = () => {
this.api('channels/unwatch', {
channel_id: this.id
}).then(() => {
this.channel.is_watching = false;
this.update();
}, e => {
alert('error');
});
};
</script> </script>
</mk-channel> </mk-channel>