This commit is contained in:
syuilo 2017-09-08 23:29:33 +09:00
parent 5c37b9cef3
commit 13a568889c
9 changed files with 228 additions and 50 deletions

View File

@ -5,6 +5,7 @@ ChangeLog (Release Notes)
unreleased
----------
* New: ユーザーページによく使うドメインを表示 (#771)
* New: よくリプライするユーザーをユーザーページに表示 (#770)
2566 (2017/09/07)
-----------------

View File

@ -499,6 +499,7 @@ mobile:
activity: "Activity"
keywords: "Keywords"
domains: "Domains"
frequently-replied-users: "Frequently talking users"
followers-you-know: "Followers you know"
last-used-at: "Latest used at"
@ -516,6 +517,10 @@ mobile:
mk-user-overview-domains:
no-domains: "No domains"
mk-user-overview-frequently-replied-users:
loading: "Loading"
no-users: "No users"
mk-user-overview-followers-you-know:
loading: "Loading"
no-users: "No users"

View File

@ -499,6 +499,7 @@ mobile:
activity: "アクティビティ"
keywords: "キーワード"
domains: "頻出ドメイン"
frequently-replied-users: "よく会話するユーザー"
followers-you-know: "知り合いのフォロワー"
last-used-at: "最終ログイン"
@ -516,6 +517,10 @@ mobile:
mk-user-overview-domains:
no-domains: "よく表れるドメインは検出されませんでした"
mk-user-overview-frequently-replied-users:
loading: "読み込み中"
no-users: "よく会話するユーザーはいません"
mk-user-overview-followers-you-know:
loading: "読み込み中"
no-users: "知り合いのユーザーはいません"

View File

@ -326,6 +326,9 @@ const endpoints: Endpoint[] = [
withCredential: true,
kind: 'account-read'
},
{
name: 'users/get_frequently_replied_users'
},
{
name: 'following/create',

View File

@ -0,0 +1,96 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Post from '../../models/post';
import User from '../../models/user';
import serialize from '../../serializers/user';
module.exports = (params, me) => new Promise(async (res, rej) => {
// Get 'user_id' parameter
const [userId, userIdErr] = $(params.user_id).id().$;
if (userIdErr) return rej('invalid user_id param');
// Lookup user
const user = await User.findOne({
_id: userId
}, {
fields: {
_id: true
}
});
if (user === null) {
return rej('user not found');
}
// Fetch recent posts
const recentPosts = await Post.find({
user_id: user._id,
reply_to_id: {
$exists: true,
$ne: null
}
}, {
sort: {
_id: -1
},
limit: 1000,
fields: {
_id: false,
reply_to_id: true
}
});
// 投稿が少なかったら中断
if (recentPosts.length === 0) {
return res([]);
}
const replyTargetPosts = await Post.find({
_id: {
$in: recentPosts.map(p => p.reply_to_id)
},
user_id: {
$ne: user._id
}
}, {
fields: {
_id: false,
user_id: true
}
});
const repliedUsers = {};
// Extract replies from recent posts
replyTargetPosts.forEach(post => {
const userId = post.user_id.toString();
if (repliedUsers[userId]) {
repliedUsers[userId]++;
} else {
repliedUsers[userId] = 1;
}
});
// Calc peak
let peak = 0;
Object.keys(repliedUsers).forEach(user => {
if (repliedUsers[user] > peak) peak = repliedUsers[user];
});
// Sort replies by frequency
const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
// Lookup top 10 replies
const topRepliedUsers = repliedUsersSorted.slice(0, 10);
// Make replies object (includes weights)
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
user: await serialize(user, me, { detail: true }),
weight: repliedUsers[user] / peak
})));
// Response
res(repliesObj);
});

View File

@ -49,3 +49,4 @@ require('./users-list.tag');
require('./user-following.tag');
require('./user-followers.tag');
require('./init-following.tag');
require('./user-card.tag');

View File

@ -1,16 +1,9 @@
<mk-init-following>
<p class="title">気になるユーザーをフォロー:</p>
<div class="users" if={ !fetching && users.length > 0 }>
<div class="user" each={ users }>
<header style={ banner_url ? 'background-image: url(' + banner_url + '?thumbnail&size=1024)' : '' }>
<a href={ '/' + username }>
<img src={ avatar_url + '?thumbnail&size=200' } alt="avatar"/>
</a>
</header>
<a class="name" href={ '/' + username } target="_blank">{ name }</a>
<p class="username">@{ username }</p>
<mk-follow-button user={ this }/>
</div>
<virtual each={ users }>
<mk-user-card user={ this } />
</virtual>
</div>
<p class="empty" if={ !fetching && users.length == 0 }>おすすめのユーザーは見つかりませんでした。</p>
<p class="fetching" if={ fetching }><i class="fa fa-spinner fa-pulse fa-fw"></i>読み込んでいます<mk-ellipsis/></p>
@ -37,49 +30,10 @@
padding 16px
background #eee
> .user
display inline-block
width 200px
text-align center
border-radius 8px
background #fff
> mk-user-card
&:not(:last-child)
margin-right 16px
> header
display block
height 80px
background-color #ddd
background-size cover
background-position center
border-radius 8px 8px 0 0
> a
> img
position absolute
top 20px
left calc(50% - 40px)
width 80px
height 80px
border solid 2px #fff
border-radius 8px
> .name
display block
margin 24px 0 0 0
font-size 16px
color #555
> .username
margin 0
font-size 15px
color #ccc
> mk-follow-button
display inline-block
margin 8px 0 16px 0
> .empty
margin 0
padding 16px

View File

@ -0,0 +1,55 @@
<mk-user-card>
<header style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=1024)' : '' }>
<a href={ '/' + user.username }>
<img src={ user.avatar_url + '?thumbnail&size=200' } alt="avatar"/>
</a>
</header>
<a class="name" href={ '/' + user.username } target="_blank">{ user.name }</a>
<p class="username">@{ user.username }</p>
<mk-follow-button user={ user }/>
<style>
:scope
display inline-block
width 200px
text-align center
border-radius 8px
background #fff
> header
display block
height 80px
background-color #ddd
background-size cover
background-position center
border-radius 8px 8px 0 0
> a
> img
position absolute
top 20px
left calc(50% - 40px)
width 80px
height 80px
border solid 2px #fff
border-radius 8px
> .name
display block
margin 24px 0 0 0
font-size 16px
color #555
> .username
margin 0
font-size 15px
color #ccc
> mk-follow-button
display inline-block
margin 8px 0 16px 0
</style>
<script>
this.user = this.opts.user;
</script>
</mk-user-card>

View File

@ -246,6 +246,12 @@
<mk-user-overview-domains user={ user }/>
</div>
</section>
<section class="frequently-replied-users">
<h2><i class="fa fa-users"></i>%i18n:mobile.tags.mk-user-overview.frequently-replied-users%</h2>
<div>
<mk-user-overview-frequently-replied-users user={ user }/>
</div>
</section>
<section class="followers-you-know" if={ SIGNIN && I.id !== user.id }>
<h2><i class="fa fa-users"></i>%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2>
<div>
@ -619,6 +625,58 @@
</script>
</mk-user-overview-domains>
<mk-user-overview-frequently-replied-users>
<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:mobile.tags.mk-user-overview-frequently-replied-users.loading%<mk-ellipsis/></p>
<div if={ !initializing && users.length > 0 }>
<virtual each={ users }>
<mk-user-card user={ this.user }/>
</virtual>
</div>
<p class="empty" if={ !initializing && users.length == 0 }>%i18n:mobile.tags.mk-user-overview-frequently-replied-users.no-users%</p>
<style>
:scope
display block
> div
overflow-x scroll
-webkit-overflow-scrolling touch
white-space nowrap
padding 8px
> mk-user-card
&:not(:last-child)
margin-right 8px
> .initializing
> .empty
margin 0
padding 16px
text-align center
color #aaa
> i
margin-right 4px
</style>
<script>
this.mixin('api');
this.user = this.opts.user;
this.initializing = true;
this.on('mount', () => {
this.api('users/get_frequently_replied_users', {
user_id: this.user.id
}).then(x => {
this.update({
users: x,
initializing: false
});
});
});
</script>
</mk-user-overview-frequently-replied-users>
<mk-user-overview-followers-you-know>
<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
<div if={ !initializing && users.length > 0 }>