Implement #770
This commit is contained in:
parent
5c37b9cef3
commit
13a568889c
9 changed files with 228 additions and 50 deletions
|
@ -5,6 +5,7 @@ ChangeLog (Release Notes)
|
||||||
unreleased
|
unreleased
|
||||||
----------
|
----------
|
||||||
* New: ユーザーページによく使うドメインを表示 (#771)
|
* New: ユーザーページによく使うドメインを表示 (#771)
|
||||||
|
* New: よくリプライするユーザーをユーザーページに表示 (#770)
|
||||||
|
|
||||||
2566 (2017/09/07)
|
2566 (2017/09/07)
|
||||||
-----------------
|
-----------------
|
||||||
|
|
|
@ -499,6 +499,7 @@ mobile:
|
||||||
activity: "Activity"
|
activity: "Activity"
|
||||||
keywords: "Keywords"
|
keywords: "Keywords"
|
||||||
domains: "Domains"
|
domains: "Domains"
|
||||||
|
frequently-replied-users: "Frequently talking users"
|
||||||
followers-you-know: "Followers you know"
|
followers-you-know: "Followers you know"
|
||||||
last-used-at: "Latest used at"
|
last-used-at: "Latest used at"
|
||||||
|
|
||||||
|
@ -516,6 +517,10 @@ mobile:
|
||||||
mk-user-overview-domains:
|
mk-user-overview-domains:
|
||||||
no-domains: "No domains"
|
no-domains: "No domains"
|
||||||
|
|
||||||
|
mk-user-overview-frequently-replied-users:
|
||||||
|
loading: "Loading"
|
||||||
|
no-users: "No users"
|
||||||
|
|
||||||
mk-user-overview-followers-you-know:
|
mk-user-overview-followers-you-know:
|
||||||
loading: "Loading"
|
loading: "Loading"
|
||||||
no-users: "No users"
|
no-users: "No users"
|
||||||
|
|
|
@ -499,6 +499,7 @@ mobile:
|
||||||
activity: "アクティビティ"
|
activity: "アクティビティ"
|
||||||
keywords: "キーワード"
|
keywords: "キーワード"
|
||||||
domains: "頻出ドメイン"
|
domains: "頻出ドメイン"
|
||||||
|
frequently-replied-users: "よく会話するユーザー"
|
||||||
followers-you-know: "知り合いのフォロワー"
|
followers-you-know: "知り合いのフォロワー"
|
||||||
last-used-at: "最終ログイン"
|
last-used-at: "最終ログイン"
|
||||||
|
|
||||||
|
@ -516,6 +517,10 @@ mobile:
|
||||||
mk-user-overview-domains:
|
mk-user-overview-domains:
|
||||||
no-domains: "よく表れるドメインは検出されませんでした"
|
no-domains: "よく表れるドメインは検出されませんでした"
|
||||||
|
|
||||||
|
mk-user-overview-frequently-replied-users:
|
||||||
|
loading: "読み込み中"
|
||||||
|
no-users: "よく会話するユーザーはいません"
|
||||||
|
|
||||||
mk-user-overview-followers-you-know:
|
mk-user-overview-followers-you-know:
|
||||||
loading: "読み込み中"
|
loading: "読み込み中"
|
||||||
no-users: "知り合いのユーザーはいません"
|
no-users: "知り合いのユーザーはいません"
|
||||||
|
|
|
@ -326,6 +326,9 @@ const endpoints: Endpoint[] = [
|
||||||
withCredential: true,
|
withCredential: true,
|
||||||
kind: 'account-read'
|
kind: 'account-read'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'users/get_frequently_replied_users'
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'following/create',
|
name: 'following/create',
|
||||||
|
|
96
src/api/endpoints/users/get_frequently_replied_users.ts
Normal file
96
src/api/endpoints/users/get_frequently_replied_users.ts
Normal 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);
|
||||||
|
});
|
|
@ -49,3 +49,4 @@ require('./users-list.tag');
|
||||||
require('./user-following.tag');
|
require('./user-following.tag');
|
||||||
require('./user-followers.tag');
|
require('./user-followers.tag');
|
||||||
require('./init-following.tag');
|
require('./init-following.tag');
|
||||||
|
require('./user-card.tag');
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
<mk-init-following>
|
<mk-init-following>
|
||||||
<p class="title">気になるユーザーをフォロー:</p>
|
<p class="title">気になるユーザーをフォロー:</p>
|
||||||
<div class="users" if={ !fetching && users.length > 0 }>
|
<div class="users" if={ !fetching && users.length > 0 }>
|
||||||
<div class="user" each={ users }>
|
<virtual each={ users }>
|
||||||
<header style={ banner_url ? 'background-image: url(' + banner_url + '?thumbnail&size=1024)' : '' }>
|
<mk-user-card user={ this } />
|
||||||
<a href={ '/' + username }>
|
</virtual>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="empty" if={ !fetching && users.length == 0 }>おすすめのユーザーは見つかりませんでした。</p>
|
<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>
|
<p class="fetching" if={ fetching }><i class="fa fa-spinner fa-pulse fa-fw"></i>読み込んでいます<mk-ellipsis/></p>
|
||||||
|
@ -37,49 +30,10 @@
|
||||||
padding 16px
|
padding 16px
|
||||||
background #eee
|
background #eee
|
||||||
|
|
||||||
> .user
|
> mk-user-card
|
||||||
display inline-block
|
|
||||||
width 200px
|
|
||||||
text-align center
|
|
||||||
border-radius 8px
|
|
||||||
background #fff
|
|
||||||
|
|
||||||
&:not(:last-child)
|
&:not(:last-child)
|
||||||
margin-right 16px
|
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
|
> .empty
|
||||||
margin 0
|
margin 0
|
||||||
padding 16px
|
padding 16px
|
||||||
|
|
55
src/web/app/mobile/tags/user-card.tag
Normal file
55
src/web/app/mobile/tags/user-card.tag
Normal 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>
|
|
@ -246,6 +246,12 @@
|
||||||
<mk-user-overview-domains user={ user }/>
|
<mk-user-overview-domains user={ user }/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 }>
|
<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>
|
<h2><i class="fa fa-users"></i>%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2>
|
||||||
<div>
|
<div>
|
||||||
|
@ -619,6 +625,58 @@
|
||||||
</script>
|
</script>
|
||||||
</mk-user-overview-domains>
|
</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>
|
<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>
|
<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 }>
|
<div if={ !initializing && users.length > 0 }>
|
||||||
|
|
Loading…
Reference in a new issue